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();
if (state.isPending) {
return (
<p className="grid min-h-dvh place-items-center font-display tracking-[0.3em] text-ink-400 uppercase">
Loading…
</p>
);
return <p className="grid min-h-dvh place-items-center font-display text-ink-400">Loading…</p>;
}
if (state.data === undefined) {
// 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") {
return (
<p className="grid min-h-dvh place-items-center font-display tracking-[0.3em] text-ink-400 uppercase">
Loading…
</p>
);
return <p className="grid min-h-dvh place-items-center font-display text-ink-400">Loading…</p>;
}
if (status === "elsewhere") {
return (

View File

@ -356,9 +356,7 @@ function Totals({ summary }: { summary: Summary }) {
>
{figures.map(([label, value, tone]) => (
<div key={label}>
<dt className="font-display text-[10px] tracking-wider text-ink-400 uppercase">
{label}
</dt>
<dt className="font-display text-xs text-ink-400">{label}</dt>
<dd className={`font-display text-base font-bold tabular-nums ${tone}`}>{value}</dd>
</div>
))}
@ -382,7 +380,7 @@ function PilotLines({ summary }: { summary: Summary }) {
})}
</span>
{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}
</span>
)}

View File

@ -57,26 +57,22 @@ export function DealModal() {
</p>
</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">
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
You get
</dt>
<dt className="font-display text-xs text-ink-400">You get</dt>
<dd aria-label="Ships and bay">
A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name}
</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">
…and {veteran.name}. {veteran.bio}
</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
aria-label="Loan amount"
className="font-display font-semibold text-amber-200 tabular-nums"
>
{formatCredits(dealLoanAmount)}
</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Repayment
</dt>
<dt className="font-display text-xs text-ink-400">Repayment</dt>
<dd aria-label="Repayment share">
{Math.round(loanRepaymentShare * 100)}% of every payout until it&apos;s paid off
</dd>

View File

@ -63,7 +63,7 @@ export function FleetAsleep() {
if (!fleetQuiet(state)) return null;
return (
<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" />
Fleet asleep — waiting for your orders
</p>

View File

@ -14,7 +14,7 @@ export function GoalsCard() {
if (next.length === 0) return null;
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">
<Icon name="target" className="size-5 text-amber-300" />
Goals

View File

@ -22,7 +22,7 @@ export function DispatchOfficePanel() {
if (state.dispatchOffice.owned) {
return (
<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" />
Dispatch Office
</p>
@ -37,8 +37,8 @@ export function DispatchOfficePanel() {
);
}
return (
<Panel tone="amber" feature className="text-sm" aria-label="Dispatch Office">
<p className="font-display font-semibold tracking-wide text-amber-100">Dispatch Office</p>
<Panel tone="amber" className="text-sm" aria-label="Dispatch Office">
<p className="font-display font-semibold text-amber-100">Dispatch Office</p>
<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
away.
@ -100,9 +100,7 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
return (
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700">
<fieldset>
<legend className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">
Flies
</legend>
<legend className="font-display text-xs text-ink-400">Flies</legend>
<div className="mt-1 flex overflow-hidden rounded-lg border border-space-600">
{(["manual", "independent"] as const).map((mode) => (
<label
@ -155,9 +153,7 @@ function OrdersEditor({ pilot }: { pilot: Pilot }) {
return (
<details className="mt-2">
<summary className="cursor-pointer font-display text-xs tracking-wider text-amber-200 uppercase">
Orders
</summary>
<summary className="cursor-pointer font-display text-xs text-amber-200">Orders</summary>
<form
aria-label={`Orders for ${pilot.name}`}
className="mt-2 space-y-3 text-sm"

View File

@ -203,7 +203,7 @@ function PilotSection({ log, state, open }: { log: PilotLog; state: PlayerState;
return (
<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">
<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>}
</summary>
{log.now && (

View File

@ -86,7 +86,7 @@ function CallBody({ call, state }: { call: RadioCall; state: PlayerState }) {
disabled={!available || answer.isPending}
onClick={() => answer.mutate({ dispatchId: call.dispatchId, optionId: option.id })}
>
<span className="font-display tracking-wide">
<span className="font-display">
{option.label}
{option.id === call.defaultOptionId && " (if you don't answer)"}
</span>
@ -151,7 +151,7 @@ export function RadioCallsPanel() {
if (state.radioCalls.length === 0) return null;
return (
<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" />
Radio calls ({state.radioCalls.length})
</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. */}
<span
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>
);

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"
>
<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 && (
<span className="text-ink-400">{booking.by === "auto" ? "Auto" : "Booked"}</span>
)}

View File

@ -136,7 +136,7 @@ export function Toasts() {
dropOverflow();
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
</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"
>
<p
aria-live="polite"
className="font-display text-[11px] font-semibold tracking-[0.2em] text-cyan-300/80 uppercase"
>
<p aria-live="polite" className="font-display text-xs font-semibold text-cyan-300/80">
{index + 1} of {cards.length}
</p>
<div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}>
<h2
id="welcome-title"
className="mt-2 font-display text-2xl font-bold tracking-wide text-amber-300"
>
<h2 id="welcome-title" className="mt-2 font-display text-2xl font-bold text-amber-300">
{card.title}
</h2>
<p className="mt-3 text-ink-200">{welcomeBody(card.body)}</p>

View File

@ -40,10 +40,68 @@
--color-problem: #ff8d7a; /* Something needs attention. */
--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);
--shadow-glow-cyan: 0 0 0 1px rgb(34 211 238 / 0.4), 0 0 22px -4px rgb(34 211 238 / 0.55);
--shadow-glow-emerald: 0 0 0 1px rgb(52 211 153 / 0.4), 0 0 22px -4px rgb(52 211 153 / 0.55);
--shadow-panel: inset 0 1px 0 0 rgb(255 255 255 / 0.05), 0 12px 32px -12px rgb(0 0 0 / 0.8);
/* Accent scales in the role hues: amber action, blue flight, mint earned, coral problem. */
--color-amber-50: #fff8e8;
--color-amber-100: #fff0d1;
--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-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. */}
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<LogoMark />
<span className="hidden font-display text-lg font-bold tracking-[0.28em] sm:inline">
DEADHEAD
</span>
<span className="hidden font-display text-[10px] tracking-[0.3em] text-cyan-300/70 uppercase lg:inline">
<span className="hidden font-display text-lg font-bold sm:inline">DEADHEAD</span>
<span className="hidden font-display text-xs text-cyan-300/70 lg:inline">
Space Freight
</span>
</div>
@ -131,15 +129,13 @@ function NavTab({
<span
aria-hidden="true"
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}
</span>
)}
</span>
<span className="font-display text-[10px] font-semibold tracking-wide uppercase sm:text-[11px] sm:tracking-wider">
{label}
</span>
<span className="font-display text-xs font-semibold text-xs">{label}</span>
</>
)}
</NavLink>

View File

@ -166,7 +166,7 @@ export function MapCanvas({
{loading && (
<p
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…
</p>

View File

@ -113,7 +113,7 @@ export function Dashboard() {
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-4">
{state.tutorial && (
<Panel feature tone="amber">
<Panel tone="amber">
<Eyebrow className="text-amber-300/80">Contract pilot</Eyebrow>
<h2 className="mt-0.5 font-display text-lg font-bold text-amber-200">
Flying for {content.tutorialCompanyName}
@ -140,10 +140,7 @@ export function Dashboard() {
aria-labelledby="on-jobs-title"
tone={onJobs.length ? "cyan" : "default"}
>
<h2
id="on-jobs-title"
className="font-display text-[11px] font-semibold tracking-[0.18em] text-cyan-300/80 uppercase"
>
<h2 id="on-jobs-title" className="font-display text-xs font-semibold text-cyan-300/80">
Ships on jobs
</h2>
{onJobs.length === 0 ? (
@ -186,7 +183,7 @@ export function Dashboard() {
<Icon name="rest" className="size-5" />
</span>
<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}
</h2>
{resting[0] ? (
@ -204,7 +201,7 @@ export function Dashboard() {
{inShop.length > 0 && (
<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}
</h2>
<ul className="mt-1 space-y-0.5 text-sm text-amber-100">
@ -227,7 +224,7 @@ export function Dashboard() {
<DispatchOfficePanel />
{!state.tutorial && (
<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" />
Logbook
</p>
@ -258,7 +255,7 @@ function YourShip({ className = "" }: { className?: string }) {
if (mine.kind === "none") {
return (
<Panel as="section" aria-label="Your ship" feature className={className}>
<Panel as="section" aria-label="Your ship" className={className}>
<Eyebrow>Your ship</Eyebrow>
<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")}`}>
@ -285,7 +282,6 @@ function YourShip({ className = "" }: { className?: string }) {
<Panel
as="section"
aria-label="Your ship"
feature
tone={dispatch ? "cyan" : "default"}
className={`flex flex-col gap-3 ${className}`}
>
@ -301,7 +297,7 @@ function YourShip({ className = "" }: { className?: string }) {
<div className="min-w-0 flex-1">
<Eyebrow>Your ship</Eyebrow>
<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 ? (
<Pill tone="cyan" icon="send">
In flight
@ -365,10 +361,8 @@ function YourShip({ className = "" }: { className?: string }) {
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"
>
<span className="font-display font-semibold tracking-wide uppercase">
Low fuel
</span>{" "}
· {locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
<span className="font-display font-semibold">Low fuel</span> ·{" "}
{locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
</p>
)}
</div>
@ -440,7 +434,7 @@ function Alerts() {
<Panel as="section" aria-labelledby="alerts-title" tone="muted">
<h2
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" />
Alerts

View File

@ -113,7 +113,7 @@ export function DevTools() {
{state.pilots.length > 0 && (
<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
aria-label="Pilot"
value={pilotId}
@ -149,7 +149,7 @@ export function DevTools() {
{state.ships.length > 0 && (
<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
aria-label="Ship"
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" />
<span className="min-w-0">
<span className="block font-display font-bold tracking-wide">
{option.shipName}
</span>
<span className="block font-display font-bold">{option.shipName}</span>
<span className="block text-xs text-ink-400">
{option.modelName} · {option.pilotName ?? "no pilot"}
</span>
@ -537,7 +535,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
function Cell({ label, value, good }: { label: string; value: string; good?: boolean }) {
return (
<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
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 className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<span className="truncate font-display text-lg font-bold tracking-wide">
{ship.name}
</span>
<span className="truncate font-display text-lg font-bold">{ship.name}</span>
{dispatch ? (
<Pill tone="cyan" icon="send">
In flight
@ -170,7 +168,7 @@ function ShipRow(props: {
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"
>
<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">
{locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
</span>
@ -383,9 +381,7 @@ export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => v
{sprite && <Art sprite={sprite} className="size-10 shrink-0" />}
<span className="min-w-0 flex-1">
<span className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">
{locationName(option.id)}
</span>
<span className="font-display font-bold">{locationName(option.id)}</span>
<span className="font-display text-sm text-ink-300 tabular-nums">
{formatCredits(option.fuelCost)} fuel used · {formatDuration(option.tripMs)}
</span>
@ -425,7 +421,7 @@ export function HullGauge({ ship }: { ship: Ship }) {
const tone = pct < needsRepairBelowPct ? "rose" : pct < noDangerousBelowPct ? "amber" : "emerald";
return (
<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 className="tracking-normal normal-case">{Math.round(pct)}%</span>
</div>
@ -436,7 +432,7 @@ export function HullGauge({ ship }: { ship: Ship }) {
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"
>
<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>
</p>
)}

View File

@ -58,7 +58,7 @@ function HqCard({ bay }: { bay: Bay }) {
);
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">
{location && (
<Art
@ -67,7 +67,7 @@ function HqCard({ bay }: { bay: Bay }) {
/>
)}
<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">
Fuel: <FuelPrice locationId={bay.locationId} />
</p>

View File

@ -116,7 +116,7 @@ export function LocationBoards({ onSelect }: { onSelect?: (contract: Contract) =
type="button"
aria-pressed={id === locationId}
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}
{locationName(id)}
@ -176,7 +176,7 @@ function LocationBoard({
const refresh = (
<p
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" />
New contracts in{" "}
@ -186,7 +186,7 @@ function LocationBoard({
{fresh.size > 0 && (
<span
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
</span>
@ -222,11 +222,10 @@ function LocationBoard({
as="li"
key={contract.id}
tone={contract.firstContact ? "amber" : "default"}
feature={contract.firstContact}
className={`flex flex-col gap-2 ${fresh.has(contract.id) ? "animate-rise" : ""}`}
>
<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)}
</span>
<span className="font-display text-lg font-bold whitespace-nowrap text-emerald-300 tabular-nums">
@ -234,7 +233,7 @@ function LocationBoard({
</span>
</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)}
</h3>
<div className="mt-1 flex flex-wrap items-center gap-1.5">

View File

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

View File

@ -74,7 +74,7 @@ function Panel({
{art}
<div className="min-w-0 flex-1">
<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
type="button"
onClick={onClose}

View File

@ -52,7 +52,7 @@ export function Market() {
{MODELS.map((model) => {
const affordable = state.company.credits >= model.price;
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="absolute inset-x-8 bottom-4 h-3 rounded-[50%] bg-cyan-300/15 blur-md" />
<Art
@ -61,7 +61,7 @@ export function Market() {
/>
</div>
<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
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 (
<div>
<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" />
{props.label}
</dt>
@ -162,7 +162,7 @@ function Reach({ model }: { model: ShipModel }) {
if (parts.length === 0) return null;
return (
<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(" · ")}
</p>
);

View File

@ -53,7 +53,7 @@ function HirePilot() {
const hasBay = state.bays.some((b) => b.locationId === bay);
return (
<Panel feature tone="amber" className="text-sm">
<Panel tone="amber" className="text-sm">
{state.bays.length === 0 ? (
<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" />
</span>
<label className="flex items-center gap-2">
<span className="font-display text-xs tracking-wider text-ink-300 uppercase">
Hire at
</span>
<span className="font-display text-xs text-ink-300">Hire at</span>
<select
aria-label="Hire at"
value={bay}
@ -121,17 +119,17 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
}`}
>
{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}
</span>
</span>
<div className="min-w-0 flex-1">
<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.isAvatar && <span className="ml-1 text-xs text-amber-300">(you)</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}
</span>
</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={`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:{" "}
<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);
return (
<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 className="tracking-normal normal-case">
{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()}`;
return (
<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}
</h2>
{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"
>
<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>
</div>
{pct >= 100 ? (
@ -143,9 +143,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
<HullRepair shipId={ship.id} atHq={atHq} />
{/* Booked from anywhere; the work starts at the HQ (CR-027). */}
{state.tutorial === null && <ServiceSection shipId={ship.id} />}
<h3 className="mt-4 font-display text-sm font-bold tracking-wider text-ink-300 uppercase">
Upgrades
</h3>
<h3 className="mt-4 font-display text-sm font-bold text-ink-300">Upgrades</h3>
<ul className="mt-2 space-y-2">
{content.upgrades.map((upgrade) => {
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"
>
<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">
Level {level}/{max}
</span>

View File

@ -39,12 +39,10 @@ export function StartScreen() {
</div>
<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
</h1>
<p className="mt-2 font-display text-sm tracking-[0.45em] text-cyan-300/80 uppercase">
Space Freight Tycoon
</p>
<p className="mt-2 font-display text-sm text-cyan-300/80">Space Freight Tycoon</p>
</div>
<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

View File

@ -46,7 +46,7 @@ export function TutorialBoard() {
return (
<div className="space-y-4">
{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" />
<div>
<p className="font-display text-base font-bold text-amber-200">
@ -106,11 +106,10 @@ export function TutorialBoard() {
data-suggested={suggested || undefined}
data-scripted={scripted ? true : undefined}
tone={scripted ? "fuchsia" : suggested ? "amber" : "default"}
feature={Boolean(scripted) || suggested}
className={`flex flex-col gap-2 ${suggested ? "shadow-glow-amber" : ""}`}
>
<div className="flex items-start justify-between gap-2">
<span className="font-display font-bold tracking-wide">
<span className="font-display font-bold">
{scripted ? (
<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">
<dl className="grid flex-1 grid-cols-3 gap-2 text-sm">
<div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase">
Pay
</dt>
<dt className="font-display text-xs text-ink-400">Pay</dt>
<dd
aria-label="Pay"
className="font-display text-base font-bold text-emerald-300 tabular-nums"
@ -154,17 +151,13 @@ export function TutorialBoard() {
</dd>
</div>
<div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase">
Time
</dt>
<dt className="font-display text-xs text-ink-400">Time</dt>
<dd aria-label="Trip time" className="font-display text-base tabular-nums">
{formatDuration(tripMs)}
</dd>
</div>
<div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase">
XP
</dt>
<dt className="font-display text-xs text-ink-400">XP</dt>
<dd
aria-label="XP"
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";
const VARIANTS: Record<ButtonVariant, string> = {
primary:
"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-space-500 bg-space-800/80 text-ink-50 hover:border-amber-400/60 hover:bg-space-700",
ghost: "text-ink-300 hover:bg-white/5 hover:text-ink-50",
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",
primary: "bg-action text-on-action hover:brightness-110",
secondary: "border border-glass-line bg-white/6 text-ink-50 hover:bg-white/12",
ghost: "text-ink-300 hover:bg-white/6 hover:text-ink-50",
success: "bg-earned text-space-950 hover:brightness-110",
danger: "border border-problem/60 bg-problem/12 text-problem hover:bg-problem/20",
};
const SIZES: Record<ButtonSize, string> = {
sm: "min-h-9 px-3 text-xs",
md: "min-h-11 px-4 text-sm",
sm: "min-h-11 px-3 text-sm",
md: "min-h-12 px-4 text-[15px]",
lg: "min-h-13 px-6 text-base",
};
@ -27,7 +24,7 @@ export function buttonClass(
block = false,
): string {
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]",
"disabled:pointer-events-none disabled:opacity-40 disabled:shadow-none disabled:saturate-50",
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";
const TONES: Record<Tone, string> = {
default: "border-space-600/70 bg-space-900/75",
muted: "border-space-700/70 bg-space-900/45",
amber: "border-amber-400/45 bg-amber-950/40",
cyan: "border-cyan-400/40 bg-cyan-950/40",
emerald: "border-emerald-400/40 bg-emerald-950/40",
rose: "border-rose-500/45 bg-rose-950/40",
fuchsia: "border-fuchsia-400/50 bg-fuchsia-950/40",
};
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",
default: "border-glass-line bg-white/5",
muted: "border-glass-line bg-transparent",
amber: "border-action/45 bg-action/8",
cyan: "border-flight/35 bg-flight/7",
emerald: "border-earned/35 bg-earned/7",
rose: "border-problem/45 bg-problem/9",
fuchsia: "border-fuchsia-400/45 bg-fuchsia-400/8",
};
export interface PanelProps extends HTMLAttributes<HTMLElement> {
as?: ElementType;
tone?: Tone;
/** Corner brackets, for panels that should stand out. */
feature?: boolean;
children?: ReactNode;
}
/** A translucent HUD panel (decision 0.5.47). */
/** A card inside a glass panel (decision 0.5.165). */
export function Panel({
as: Tag = "div",
tone = "default",
feature = false,
className = "",
children,
...rest
}: PanelProps) {
return (
<Tag
className={`relative rounded-xl border p-4 shadow-panel backdrop-blur-md ${TONES[tone]} ${className}`}
{...rest}
>
{feature && <Brackets tone={tone} />}
<Tag className={`relative rounded-2xl border p-4 ${TONES[tone]} ${className}`} {...rest}>
{children}
</Tag>
);
}
function Brackets({ tone }: { tone: Tone }) {
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. */
/** A small label above a value or section. */
export function Eyebrow({ children, className = "" }: { children: ReactNode; className?: string }) {
return (
<div
className={`font-display text-[11px] font-semibold tracking-[0.18em] text-ink-400 uppercase ${className}`}
>
{children}
</div>
);
return <div className={`text-xs font-semibold text-ink-400 ${className}`}>{children}</div>;
}

View File

@ -40,26 +40,22 @@ export function Sheet({
}) {
return createPortal(
<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
role="dialog"
aria-modal="true"
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
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 && (
<div className="font-display text-[11px] font-semibold tracking-[0.2em] text-cyan-300/80 uppercase">
{eyebrow}
</div>
)}
{eyebrow && <div className="text-xs font-semibold text-ink-400">{eyebrow}</div>}
<h2
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}
</h2>

View File

@ -4,12 +4,12 @@ import { Icon, type IconName } from "./icons";
import type { Tone } from "./Panel";
const PILL_TONES: Record<Tone, string> = {
default: "border-space-500 bg-space-800 text-ink-200",
muted: "border-space-600 bg-space-900 text-ink-400",
amber: "border-amber-400/50 bg-amber-500/15 text-amber-200",
cyan: "border-cyan-400/50 bg-cyan-500/15 text-cyan-200",
emerald: "border-emerald-400/50 bg-emerald-500/15 text-emerald-200",
rose: "border-rose-400/50 bg-rose-500/15 text-rose-200",
default: "border-glass-line bg-white/6 text-ink-200",
muted: "border-glass-line bg-transparent text-ink-400",
amber: "border-action/50 bg-action/12 text-action",
cyan: "border-flight/45 bg-flight/12 text-flight",
emerald: "border-earned/45 bg-earned/12 text-earned",
rose: "border-problem/50 bg-problem/12 text-problem",
fuchsia: "border-fuchsia-400/50 bg-fuchsia-500/15 text-fuchsia-200",
};
@ -27,7 +27,7 @@ export function Pill({
}) {
return (
<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" />}
{children}
@ -36,11 +36,12 @@ export function Pill({
}
const METER_FILLS: Partial<Record<Tone, string>> = {
amber: "from-amber-500 to-amber-300 shadow-[0_0_10px] shadow-amber-400/60",
cyan: "from-cyan-500 to-sky-300 shadow-[0_0_10px] shadow-cyan-400/60",
emerald: "from-emerald-500 to-emerald-300 shadow-[0_0_10px] shadow-emerald-400/60",
rose: "from-rose-600 to-rose-400 shadow-[0_0_10px] shadow-rose-500/60",
fuchsia: "from-fuchsia-500 to-fuchsia-300",
amber: "bg-action",
cyan: "bg-flight",
emerald: "bg-earned",
rose: "bg-problem",
fuchsia: "bg-fuchsia-300",
default: "bg-ink-300",
};
/** A labelled `progressbar`. `label` is its accessible name. */
@ -69,10 +70,10 @@ export function Meter({
aria-valuemin={min}
aria-valuemax={max}
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
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}%` }}
/>
</div>
@ -104,16 +105,14 @@ export function StatTile({
className?: string;
}) {
return (
<div
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 font-display text-[11px] font-semibold tracking-[0.16em] text-ink-400 uppercase">
<div className={`rounded-2xl border border-glass-line bg-white/5 px-3 py-2.5 ${className}`}>
<div className="flex items-center gap-1.5 text-xs font-semibold text-ink-400">
{icon && <Icon name={icon} className="size-3.5" />}
{label}
</div>
<div
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}
</div>
@ -138,20 +137,10 @@ export function ScreenHeader({
return (
<div className="flex items-end justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
{icon && (
<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>
)}
{icon && <Icon name={icon} className="size-6 shrink-0 text-ink-300" />}
<div className="min-w-0">
{eyebrow && (
<div className="font-display text-[11px] font-semibold tracking-[0.22em] text-cyan-300/80 uppercase">
{eyebrow}
</div>
)}
<h1 className="font-display text-2xl leading-tight font-bold tracking-wide uppercase">
{title}
</h1>
{eyebrow && <div className="text-xs font-semibold text-ink-400">{eyebrow}</div>}
<h1 className="font-title text-xl leading-tight">{title}</h1>
{subtitle && <p className="text-sm text-ink-300">{subtitle}</p>}
</div>
</div>