Cyberpunk visual overhaul

Dark CARTO basemap (was default bright OSM tiles), Orbitron/Share Tech
Mono fonts, neon cyan/magenta HUD chrome with glassmorphism panels,
glowing divIcon portal markers (pulsing when claimed) replacing plain
circle dots, neon drop-shadow filters on links/fields, and a subtle
scanline overlay. Login and team-pick screens restyled to match.
This commit is contained in:
2026-08-05 14:14:38 +00:00
parent cd3d82d1c1
commit df9a4ae74b
7 changed files with 237 additions and 84 deletions
+6
View File
@@ -4,6 +4,12 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Regress</title> <title>Regress</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Share+Tech+Mono&display=swap"
rel="stylesheet"
/>
<!-- No-op outside an Archipelago iframe (see nostr-provider.js's own <!-- No-op outside an Archipelago iframe (see nostr-provider.js's own
window === window.top guard) — safe to always include. Provides window === window.top guard) — safe to always include. Provides
window.nostr + auto sign-in via the node's selected nostr identity window.nostr + auto sign-in via the node's selected nostr identity
+3 -3
View File
@@ -10,9 +10,9 @@ onMounted(() => auth.ensureLoaded());
</script> </script>
<template> <template>
<div class="h-full w-full bg-neutral-950 text-white"> <div class="h-full w-full bg-void text-white">
<div v-if="!auth.loaded" class="flex h-full items-center justify-center text-neutral-400"> <div v-if="!auth.loaded" class="flex h-full items-center justify-center font-display text-cyan-neon text-glow-cyan">
Loading INITIALIZING
</div> </div>
<LoginView v-else-if="!auth.pubkey" /> <LoginView v-else-if="!auth.pubkey" />
<TeamPickView v-else-if="!auth.team" /> <TeamPickView v-else-if="!auth.team" />
+77
View File
@@ -6,3 +6,80 @@ html, body, #app {
height: 100%; height: 100%;
margin: 0; margin: 0;
} }
body {
background: #050508;
color: #d8fbff;
font-family: 'Share Tech Mono', monospace;
}
h1, h2, .font-display {
font-family: 'Orbitron', sans-serif;
}
/* Subtle animated scanline field over the whole app — cheap, pure-CSS. */
#app::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2000;
background-image: repeating-linear-gradient(
0deg,
rgba(0, 255, 242, 0.035) 0px,
rgba(0, 255, 242, 0.035) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: screen;
}
.text-glow-cyan { text-shadow: 0 0 8px rgba(0, 255, 242, 0.85); }
.text-glow-orange { text-shadow: 0 0 8px rgba(255, 95, 31, 0.9); }
.text-glow-green { text-shadow: 0 0 8px rgba(57, 255, 20, 0.9); }
.hud-panel {
background: rgba(8, 10, 20, 0.88);
backdrop-filter: blur(10px);
border: 1px solid rgba(0, 255, 242, 0.35);
box-shadow: 0 0 20px rgba(0, 255, 242, 0.12), inset 0 0 30px rgba(0, 255, 242, 0.04);
clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
}
/* Darken + tint the CARTO dark tiles a bit further and desaturate slightly
so our neon markers/links pop harder against the basemap. */
.leaflet-tile-pane {
filter: brightness(0.75) saturate(1.3) hue-rotate(-6deg);
}
.leaflet-container {
background: #050508 !important;
}
.leaflet-popup-content-wrapper,
.leaflet-tooltip {
background: rgba(8, 10, 20, 0.92) !important;
color: #d8fbff !important;
border: 1px solid rgba(0, 255, 242, 0.4) !important;
font-family: 'Share Tech Mono', monospace !important;
box-shadow: 0 0 12px rgba(0, 255, 242, 0.25) !important;
}
.leaflet-popup-tip {
background: rgba(8, 10, 20, 0.92) !important;
}
.regress-portal-marker {
filter: drop-shadow(0 0 4px var(--portal-glow, #9ca3af));
}
.regress-portal-marker.claimed {
animation: portal-pulse 2.4s ease-in-out infinite;
}
@keyframes portal-pulse {
0%, 100% { filter: drop-shadow(0 0 4px var(--portal-glow, #9ca3af)); }
50% { filter: drop-shadow(0 0 10px var(--portal-glow, #9ca3af)); }
}
.regress-link-orange { filter: drop-shadow(0 0 4px #ff5f1f); }
.regress-link-green { filter: drop-shadow(0 0 4px #39ff14); }
.regress-field-orange { filter: drop-shadow(0 0 6px #ff5f1f); }
.regress-field-green { filter: drop-shadow(0 0 6px #39ff14); }
+20 -18
View File
@@ -36,32 +36,34 @@ async function handleNsecLogin() {
</script> </script>
<template> <template>
<div class="flex h-full flex-col items-center justify-center gap-4 px-6 text-center"> <div class="relative flex h-full flex-col items-center justify-center gap-5 overflow-hidden px-6 text-center">
<h1 class="text-3xl font-bold">Regress</h1> <div
<p class="max-w-sm text-neutral-400"> class="pointer-events-none absolute inset-0"
Claim real Bitcoin-accepting businesses for your team. Log in with Nostr to start. style="background: radial-gradient(circle at 50% 40%, rgba(0,255,242,0.08), transparent 60%)"
></div>
<h1 class="font-display text-5xl font-black tracking-[0.3em] text-cyan-neon text-glow-cyan">REGRESS</h1>
<p class="max-w-sm text-cyan-neon/60">
Real-world nodes. Two factions. Claim them for orange or green sign in with Nostr to jack in.
</p> </p>
<button <button
class="rounded-lg bg-white px-6 py-3 font-semibold text-black transition hover:bg-neutral-200 disabled:opacity-50" class="rounded border-2 border-cyan-neon px-6 py-3 font-display font-bold uppercase tracking-wider text-cyan-neon transition hover:bg-cyan-neon/10 hover:shadow-glow-cyan disabled:opacity-50"
:disabled="loggingIn" :disabled="loggingIn"
@click="handleLogin" @click="handleLogin"
> >
{{ loggingIn ? 'Signing in…' : 'Log in with Nostr' }} {{ loggingIn ? 'Authenticating…' : 'Connect with Nostr' }}
</button> </button>
<p v-if="!hasNip07()" class="text-sm text-neutral-500"> <p v-if="!hasNip07()" class="text-sm text-cyan-neon/40">
No NIP-07 extension detected install Alby or nos2x, or open this app from the Archipelago dashboard. No NIP-07 extension detected install Alby or nos2x, or open this app from the Archipelago dashboard.
</p> </p>
<button <button class="text-xs text-cyan-neon/30 underline hover:text-magenta-neon" @click="showNsecForm = !showNsecForm">
class="text-xs text-neutral-600 underline hover:text-neutral-400"
@click="showNsecForm = !showNsecForm"
>
{{ showNsecForm ? 'Hide' : 'Or paste an nsec (unsafe)' }} {{ showNsecForm ? 'Hide' : 'Or paste an nsec (unsafe)' }}
</button> </button>
<div v-if="showNsecForm" class="flex w-full max-w-xs flex-col gap-2"> <div v-if="showNsecForm" class="hud-panel flex w-full max-w-xs flex-col gap-2 p-4">
<p class="rounded bg-red-950 px-3 py-2 text-left text-xs text-red-300"> <p class="rounded border border-magenta-neon/50 bg-magenta-neon/10 px-3 py-2 text-left text-xs text-magenta-neon">
Unsafe: your private key is decoded and used to sign right in this UNSAFE: your private key is decoded and used to sign right in this
page. Only paste a throwaway/test nsec never your real identity's. page. Only paste a throwaway/test nsec never your real identity's.
The key isn't saved anywhere; it's used once and discarded. The key isn't saved anywhere; it's used once and discarded.
</p> </p>
@@ -69,18 +71,18 @@ async function handleNsecLogin() {
v-model="nsecInput" v-model="nsecInput"
type="password" type="password"
placeholder="nsec1…" placeholder="nsec1…"
class="rounded bg-neutral-900 px-3 py-2 text-sm text-white outline-none ring-1 ring-neutral-700 focus:ring-red-500" class="rounded border border-cyan-neon/30 bg-black/60 px-3 py-2 text-sm text-cyan-neon outline-none focus:border-magenta-neon"
@keyup.enter="handleNsecLogin" @keyup.enter="handleNsecLogin"
/> />
<button <button
class="rounded bg-red-900 px-3 py-2 text-sm font-semibold text-red-100 transition hover:bg-red-800 disabled:opacity-50" class="rounded border border-magenta-neon px-3 py-2 text-sm font-bold uppercase text-magenta-neon transition hover:shadow-glow-magenta disabled:opacity-50"
:disabled="loggingIn || !nsecInput" :disabled="loggingIn || !nsecInput"
@click="handleNsecLogin" @click="handleNsecLogin"
> >
{{ loggingIn ? 'Signing in' : 'Log in with nsec (unsafe)' }} {{ loggingIn ? 'Authenticating' : 'Connect with nsec (unsafe)' }}
</button> </button>
</div> </div>
<p v-if="error" class="text-sm text-red-400">{{ error }}</p> <p v-if="error" class="text-sm text-magenta-neon">{{ error }}</p>
</div> </div>
</template> </template>
+95 -55
View File
@@ -26,13 +26,25 @@ const linkSourcePlace = computed<Place | null>(
() => game.places.find((p) => p.id === linkSourceId.value) ?? null, () => game.places.find((p) => p.id === linkSourceId.value) ?? null,
); );
const TEAM_COLOR: Record<string, string> = { orange: '#f97316', green: '#22c55e' }; const TEAM_COLOR: Record<string, string> = { orange: '#ff5f1f', green: '#39ff14' };
const NEUTRAL_COLOR = '#9ca3af'; const NEUTRAL_COLOR = '#3fb8c9';
function colorFor(team: string | null): string { function colorFor(team: string | null): string {
return team ? TEAM_COLOR[team] : NEUTRAL_COLOR; return team ? TEAM_COLOR[team] : NEUTRAL_COLOR;
} }
function portalIcon(place: Place, isSource: boolean): L.DivIcon {
const color = isSource ? '#ffffff' : colorFor(place.claim_team);
const size = isSource ? 22 : place.claim_team ? 18 : 14;
const claimedClass = place.claim_team ? 'claimed' : '';
return L.divIcon({
className: `regress-portal-marker ${claimedClass}`,
html: `<div style="--portal-glow:${color}; width:${size}px; height:${size}px; border-radius:50%; background:radial-gradient(circle at 35% 35%, ${color}, ${color}33 70%); border:2px solid ${color};"></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
}
function redraw() { function redraw() {
if (!map || !markersLayer || !linksLayer || !fieldsLayer) return; if (!map || !markersLayer || !linksLayer || !fieldsLayer) return;
markersLayer.clearLayers(); markersLayer.clearLayers();
@@ -43,13 +55,7 @@ function redraw() {
for (const place of game.places) { for (const place of game.places) {
const isSource = place.id === linkSourceId.value; const isSource = place.id === linkSourceId.value;
const marker = L.circleMarker([place.lat, place.lon], { const marker = L.marker([place.lat, place.lon], { icon: portalIcon(place, isSource) });
radius: isSource ? 10 : 7,
color: isSource ? '#ffffff' : colorFor(place.claim_team),
weight: isSource ? 3 : 2,
fillColor: colorFor(place.claim_team),
fillOpacity: 0.85,
});
marker.on('click', () => onMarkerClick(place)); marker.on('click', () => onMarkerClick(place));
marker.bindTooltip(place.name || `Place ${place.id}`, { direction: 'top' }); marker.bindTooltip(place.name || `Place ${place.id}`, { direction: 'top' });
markersLayer.addLayer(marker); markersLayer.addLayer(marker);
@@ -65,7 +71,12 @@ function redraw() {
[from.lat, from.lon], [from.lat, from.lon],
[to.lat, to.lon], [to.lat, to.lon],
], ],
{ color: TEAM_COLOR[link.team], weight: 2, opacity: 0.8 }, {
color: TEAM_COLOR[link.team],
weight: 2,
opacity: 0.9,
className: link.team === 'orange' ? 'regress-link-orange' : 'regress-link-green',
},
), ),
); );
} }
@@ -77,7 +88,13 @@ function redraw() {
.map((p) => [p.lat, p.lon] as [number, number]); .map((p) => [p.lat, p.lon] as [number, number]);
if (points.length !== 3) continue; if (points.length !== 3) continue;
fieldsLayer.addLayer( fieldsLayer.addLayer(
L.polygon(points, { color: TEAM_COLOR[field.team], weight: 1, fillOpacity: 0.15, stroke: false }), L.polygon(points, {
color: TEAM_COLOR[field.team],
weight: 1,
fillOpacity: 0.18,
stroke: false,
className: field.team === 'orange' ? 'regress-field-orange' : 'regress-field-green',
}),
); );
} }
} }
@@ -139,9 +156,11 @@ const greenScore = computed(() => game.scores.find((s) => s.team === 'green'));
onMounted(async () => { onMounted(async () => {
if (mapEl.value) { if (mapEl.value) {
map = L.map(mapEl.value).setView([32.7607, -16.9595], 12); map = L.map(mapEl.value, { zoomControl: false }).setView([32.7607, -16.9595], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.control.zoom({ position: 'bottomright' }).addTo(map);
attribution: '&copy; OpenStreetMap contributors', L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap contributors &copy; CARTO',
subdomains: 'abcd',
maxZoom: 19, maxZoom: 19,
}).addTo(map); }).addTo(map);
fieldsLayer = L.layerGroup().addTo(map); fieldsLayer = L.layerGroup().addTo(map);
@@ -157,29 +176,41 @@ watch(linkSourceId, redraw);
</script> </script>
<template> <template>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col bg-void">
<header class="flex items-center justify-between gap-4 bg-neutral-900 px-4 py-2 text-sm"> <header class="hud-panel z-[1000] m-2 flex items-center justify-between gap-4 px-4 py-2 text-sm">
<div class="flex items-center gap-4"> <div class="flex items-center gap-5">
<span class="font-bold">Regress</span> <span class="font-display text-lg font-black tracking-widest text-cyan-neon text-glow-cyan">REGRESS</span>
<span class="flex items-center gap-1 text-orange-500"> <span class="flex items-center gap-1 font-bold text-orange-neon text-glow-orange">
{{ orangeScore?.claimedPlaces ?? 0 }} places · {{ orangeScore?.fields ?? 0 }} fields · {{ orangeScore?.claimedPlaces ?? 0 }} <span class="text-orange-neon/60">nodes</span> ·
{{ orangeScore?.fields ?? 0 }} <span class="text-orange-neon/60">fields</span> ·
{{ (orangeScore?.areaKm2 ?? 0).toFixed(2) }} km² {{ (orangeScore?.areaKm2 ?? 0).toFixed(2) }} km²
</span> </span>
<span class="flex items-center gap-1 text-green-500"> <span class="flex items-center gap-1 font-bold text-green-neon text-glow-green">
{{ greenScore?.claimedPlaces ?? 0 }} places · {{ greenScore?.fields ?? 0 }} fields · {{ greenScore?.claimedPlaces ?? 0 }} <span class="text-green-neon/60">nodes</span> ·
{{ greenScore?.fields ?? 0 }} <span class="text-green-neon/60">fields</span> ·
{{ (greenScore?.areaKm2 ?? 0).toFixed(2) }} km² {{ (greenScore?.areaKm2 ?? 0).toFixed(2) }} km²
</span> </span>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="text-neutral-400"> <span class="text-cyan-neon/70">
{{ auth.displayName ?? auth.pubkey?.slice(0, 8) }} · {{ auth.displayName ?? auth.pubkey?.slice(0, 8) }}//<span
<span :class="auth.team === 'orange' ? 'text-orange-500' : 'text-green-500'">{{ auth.team }}</span> :class="auth.team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
class="font-bold uppercase"
>{{ auth.team }}</span
>
</span> </span>
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" :disabled="actionBusy" @click="doSync"> <button
Sync from BTC Map class="rounded border border-cyan-neon/50 bg-black/40 px-3 py-1 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan"
:disabled="actionBusy"
@click="doSync"
>
Sync BTC Map
</button> </button>
<button class="rounded bg-neutral-800 px-3 py-1 hover:bg-neutral-700" @click="auth.logout()"> <button
Log out class="rounded border border-magenta-neon/50 bg-black/40 px-3 py-1 text-magenta-neon transition hover:border-magenta-neon hover:shadow-glow-magenta"
@click="auth.logout()"
>
Disconnect
</button> </button>
</div> </div>
</header> </header>
@@ -187,35 +218,40 @@ watch(linkSourceId, redraw);
<div class="relative flex-1"> <div class="relative flex-1">
<div ref="mapEl" class="h-full w-full"></div> <div ref="mapEl" class="h-full w-full"></div>
<div <div v-if="linkSourcePlace" class="hud-panel absolute left-4 top-4 z-[1000] px-4 py-2 text-sm text-cyan-neon">
v-if="linkSourcePlace" LINKING FROM <strong class="text-glow-cyan">{{ linkSourcePlace.name }}</strong> select a friendly node, or
class="absolute left-4 top-4 z-[1000] rounded-lg bg-neutral-900/95 px-4 py-2 text-sm shadow-lg" <button class="ml-2 text-magenta-neon underline" @click="cancelLink">abort</button>
>
Linking from <strong>{{ linkSourcePlace.name }}</strong> click a friendly place to complete, or
<button class="ml-2 text-neutral-400 underline" @click="cancelLink">cancel</button>
</div> </div>
<div <div v-if="selectedPlace" class="hud-panel absolute right-4 top-4 z-[1000] w-72 p-4 text-sm">
v-if="selectedPlace"
class="absolute right-4 top-4 z-[1000] w-72 rounded-lg bg-neutral-900/95 p-4 text-sm shadow-lg"
>
<div class="mb-1 flex items-start justify-between gap-2"> <div class="mb-1 flex items-start justify-between gap-2">
<h2 class="font-bold">{{ selectedPlace.name || `Place ${selectedPlace.id}` }}</h2> <h2 class="font-display font-bold text-cyan-neon text-glow-cyan">
<button class="text-neutral-500" @click="selectedPlaceId = null"></button> {{ selectedPlace.name || `Node ${selectedPlace.id}` }}
</h2>
<button class="text-cyan-neon/50 hover:text-cyan-neon" @click="selectedPlaceId = null"></button>
</div> </div>
<p v-if="selectedPlace.address" class="mb-2 text-neutral-400">{{ selectedPlace.address }}</p> <p v-if="selectedPlace.address" class="mb-2 text-cyan-neon/50">{{ selectedPlace.address }}</p>
<p class="mb-3"> <p class="mb-3">
Status: STATUS:
<span v-if="!selectedPlace.claim_team" class="text-neutral-400">unclaimed</span> <span v-if="!selectedPlace.claim_team" class="text-cyan-neon/60">UNCLAIMED</span>
<span v-else :class="selectedPlace.claim_team === 'orange' ? 'text-orange-500' : 'text-green-500'"> <span
claimed by {{ selectedPlace.claim_team }} v-else
class="font-bold uppercase"
:class="selectedPlace.claim_team === 'orange' ? 'text-orange-neon text-glow-orange' : 'text-green-neon text-glow-green'"
>
held by {{ selectedPlace.claim_team }}
</span> </span>
</p> </p>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<button <button
v-if="selectedPlace.claim_team !== auth.team" v-if="selectedPlace.claim_team !== auth.team"
class="rounded bg-white px-3 py-2 font-semibold text-black hover:bg-neutral-200 disabled:opacity-50" class="rounded border px-3 py-2 font-bold uppercase transition disabled:opacity-50"
:class="
auth.team === 'orange'
? 'border-orange-neon text-orange-neon hover:shadow-glow-orange'
: 'border-green-neon text-green-neon hover:shadow-glow-green'
"
:disabled="actionBusy" :disabled="actionBusy"
@click="doClaim(selectedPlace)" @click="doClaim(selectedPlace)"
> >
@@ -223,7 +259,7 @@ watch(linkSourceId, redraw);
</button> </button>
<button <button
v-else-if="linkSourceId !== selectedPlace.id" v-else-if="linkSourceId !== selectedPlace.id"
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50" class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
:disabled="actionBusy" :disabled="actionBusy"
@click="startLink(selectedPlace)" @click="startLink(selectedPlace)"
> >
@@ -231,7 +267,7 @@ watch(linkSourceId, redraw);
</button> </button>
<button <button
v-if="linkSourceId && linkSourceId !== selectedPlace.id && selectedPlace.claim_team === auth.team" v-if="linkSourceId && linkSourceId !== selectedPlace.id && selectedPlace.claim_team === auth.team"
class="rounded bg-neutral-800 px-3 py-2 hover:bg-neutral-700 disabled:opacity-50" class="rounded border border-cyan-neon/50 px-3 py-2 text-cyan-neon transition hover:border-cyan-neon hover:shadow-glow-cyan disabled:opacity-50"
:disabled="actionBusy" :disabled="actionBusy"
@click="completeLink(selectedPlace)" @click="completeLink(selectedPlace)"
> >
@@ -239,17 +275,21 @@ watch(linkSourceId, redraw);
</button> </button>
</div> </div>
<p v-if="actionError" class="mt-3 text-red-400">{{ actionError }}</p> <p v-if="actionError" class="mt-3 text-magenta-neon">{{ actionError }}</p>
</div> </div>
<div <div
v-if="!game.places.length && !game.loading" v-if="!game.places.length && !game.loading"
class="absolute inset-0 z-[999] flex items-center justify-center bg-black/60" class="absolute inset-0 z-[999] flex items-center justify-center bg-black/70"
> >
<div class="rounded-lg bg-neutral-900 p-6 text-center"> <div class="hud-panel p-6 text-center">
<p class="mb-3">No places loaded yet pull the current Madeira dataset from BTC Map.</p> <p class="mb-3 text-cyan-neon">NO NODES DETECTED pull the live Madeira dataset from BTC Map.</p>
<button class="rounded bg-white px-4 py-2 font-semibold text-black" :disabled="actionBusy" @click="doSync"> <button
Sync from BTC Map class="rounded border border-cyan-neon px-4 py-2 font-bold text-cyan-neon hover:shadow-glow-cyan"
:disabled="actionBusy"
@click="doSync"
>
Sync from BTC Map
</button> </button>
</div> </div>
</div> </div>
+6 -6
View File
@@ -22,26 +22,26 @@ async function pick(team: Team) {
<template> <template>
<div class="flex h-full flex-col items-center justify-center gap-6 px-6 text-center"> <div class="flex h-full flex-col items-center justify-center gap-6 px-6 text-center">
<h1 class="text-2xl font-bold">Pick your faction</h1> <h1 class="font-display text-3xl font-black tracking-widest text-cyan-neon text-glow-cyan">CHOOSE YOUR FACTION</h1>
<p class="max-w-sm text-neutral-400"> <p class="max-w-sm text-cyan-neon/60">
This choice is permanent you can't switch teams later, so pick the side you're actually playing for. This choice is permanent you can't switch teams later, so pick the side you're actually playing for.
</p> </p>
<div class="flex gap-4"> <div class="flex gap-6">
<button <button
class="rounded-lg bg-orange-500 px-8 py-4 text-lg font-bold text-black transition hover:bg-orange-400 disabled:opacity-50" class="rounded border-2 border-orange-neon px-10 py-6 font-display text-xl font-black uppercase tracking-wider text-orange-neon transition hover:bg-orange-neon/10 hover:shadow-glow-orange disabled:opacity-50"
:disabled="picking" :disabled="picking"
@click="pick('orange')" @click="pick('orange')"
> >
Orange Orange
</button> </button>
<button <button
class="rounded-lg bg-green-500 px-8 py-4 text-lg font-bold text-black transition hover:bg-green-400 disabled:opacity-50" class="rounded border-2 border-green-neon px-10 py-6 font-display text-xl font-black uppercase tracking-wider text-green-neon transition hover:bg-green-neon/10 hover:shadow-glow-green disabled:opacity-50"
:disabled="picking" :disabled="picking"
@click="pick('green')" @click="pick('green')"
> >
Green Green
</button> </button>
</div> </div>
<p v-if="error" class="text-sm text-red-400">{{ error }}</p> <p v-if="error" class="text-sm text-magenta-neon">{{ error }}</p>
</div> </div>
</template> </template>
+30 -2
View File
@@ -3,9 +3,37 @@ export default {
content: ['./index.html', './src/**/*.{vue,ts}'], content: ['./index.html', './src/**/*.{vue,ts}'],
theme: { theme: {
extend: { extend: {
fontFamily: {
display: ['Orbitron', 'sans-serif'],
mono: ['"Share Tech Mono"', 'monospace'],
},
colors: { colors: {
orange: { team: '#f97316' }, void: '#050508',
green: { team: '#22c55e' }, panel: '#0b0d16',
cyan: { neon: '#00fff2' },
magenta: { neon: '#ff00e5' },
orange: { neon: '#ff5f1f' },
green: { neon: '#39ff14' },
},
boxShadow: {
'glow-cyan': '0 0 6px #00fff2, 0 0 18px rgba(0,255,242,0.5)',
'glow-magenta': '0 0 6px #ff00e5, 0 0 18px rgba(255,0,229,0.5)',
'glow-orange': '0 0 6px #ff5f1f, 0 0 18px rgba(255,95,31,0.6)',
'glow-green': '0 0 6px #39ff14, 0 0 18px rgba(57,255,20,0.6)',
},
animation: {
'pulse-glow': 'pulse-glow 2.4s ease-in-out infinite',
scan: 'scan 6s linear infinite',
},
keyframes: {
'pulse-glow': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.55 },
},
scan: {
'0%': { backgroundPosition: '0 0' },
'100%': { backgroundPosition: '0 64px' },
},
}, },
}, },
}, },