diff --git a/web-ui/src/routes/detections.tsx b/web-ui/src/routes/detections.tsx
index 9ac4550b..37aeb93a 100644
--- a/web-ui/src/routes/detections.tsx
+++ b/web-ui/src/routes/detections.tsx
@@ -48,11 +48,15 @@ function Detections() {
- {detection.Date}
- {detection.Time}
+
+ {detection.Date}
+
+
+ {detection.Time}
+
{detection.Com_Name}
{detection.Sci_Name}
-
+
{detection.Confidence == null
? "—"
: `${Math.round(detection.Confidence * 100)}%`}
diff --git a/web-ui/src/routes/index.tsx b/web-ui/src/routes/index.tsx
index fa3dfc75..da4595a8 100644
--- a/web-ui/src/routes/index.tsx
+++ b/web-ui/src/routes/index.tsx
@@ -1,5 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
+import { HeroBand } from "#/components/HeroBand.tsx";
import {
Card,
CardContent,
@@ -32,7 +33,8 @@ function Home() {
return (
-
+
+
What's singing right now
@@ -47,7 +49,9 @@ function Home() {
- Recent detections
+
+ Recent detections
+
@@ -71,12 +75,12 @@ function Home() {
-
+
{detection.Date} {detection.Time}
{detection.Com_Name}
{detection.Sci_Name}
-
+
{detection.Confidence == null
? "—"
: `${Math.round(detection.Confidence * 100)}%`}
@@ -95,7 +99,9 @@ function StatCard({ label, value }: { label: string; value: number }) {
return (
- {value}
+
+ {value}
+
{label}
diff --git a/web-ui/src/routes/species.tsx b/web-ui/src/routes/species.tsx
index 377a23f5..714a0bf8 100644
--- a/web-ui/src/routes/species.tsx
+++ b/web-ui/src/routes/species.tsx
@@ -48,9 +48,13 @@ function Species() {
{s.comName}
{s.sciName}
- {s.lastDetected}
+
+ {s.lastDetected}
+
- {s.count}
+
+ {s.count}
+
))
diff --git a/web-ui/src/styles.css b/web-ui/src/styles.css
index 3295f797..63a65605 100644
--- a/web-ui/src/styles.css
+++ b/web-ui/src/styles.css
@@ -1,124 +1,65 @@
-@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Manrope:wght@400;500;600;700;800&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Manrope:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@import 'tw-animate-css';
-@custom-variant dark (&:is(.dark *));
-
:root {
- --sea-ink: #173a40;
- --sea-ink-soft: #416166;
- --lagoon: #4fb8b2;
- --lagoon-deep: #328f97;
- --palm: #2f6a4a;
- --sand: #e7f0e8;
- --foam: #f3faf5;
- --surface: rgba(255, 255, 255, 0.74);
- --surface-strong: rgba(255, 255, 255, 0.9);
- --line: rgba(23, 58, 64, 0.14);
- --inset-glint: rgba(255, 255, 255, 0.82);
- --kicker: rgba(47, 106, 74, 0.9);
- --bg-base: #e7f3ec;
- --header-bg: rgba(251, 255, 248, 0.84);
- --chip-bg: rgba(255, 255, 255, 0.8);
- --chip-line: rgba(47, 106, 74, 0.18);
- --link-bg-hover: rgba(255, 255, 255, 0.9);
- --hero-a: rgba(79, 184, 178, 0.36);
- --hero-b: rgba(47, 106, 74, 0.2);
-
- --background: oklch(1 0 0);
- --foreground: oklch(0.141 0.005 285.823);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.141 0.005 285.823);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.141 0.005 285.823);
- --primary: oklch(0.21 0.006 285.885);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.967 0.001 286.375);
- --secondary-foreground: oklch(0.21 0.006 285.885);
- --muted: oklch(0.967 0.001 286.375);
- --muted-foreground: oklch(0.552 0.016 285.938);
- --accent: oklch(0.967 0.001 286.375);
- --accent-foreground: oklch(0.21 0.006 285.885);
- --destructive: oklch(0.577 0.245 27.325);
- --destructive-foreground: oklch(0.577 0.245 27.325);
- --border: oklch(0.92 0.004 286.32);
- --input: oklch(0.92 0.004 286.32);
- --ring: oklch(0.871 0.006 286.286);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
+ /* Named palette -- a field naturalist's paper, not a screen glow. */
+ --paper: #f3ede0;
+ --paper-raised: #faf6ec;
+ --ink: #26301f;
+ --moss: #4f6144;
+ --sage: #a7b494;
+ --sand: #c69a58;
+ --clay: #9c4a34;
+
+ --surface: color-mix(in oklab, var(--paper-raised) 88%, white 12%);
+ --surface-strong: var(--paper-raised);
+ --line: color-mix(in oklab, var(--moss) 22%, transparent);
+ --inset-glint: color-mix(in oklab, white 70%, var(--paper-raised));
+ --kicker: var(--moss);
+ --header-bg: color-mix(in oklab, var(--paper) 92%, var(--paper-raised) 8%);
+ --link-bg-hover: color-mix(in oklab, var(--sage) 18%, var(--paper-raised));
+
+ --background: var(--paper);
+ --foreground: var(--ink);
+ --card: var(--paper-raised);
+ --card-foreground: var(--ink);
+ --popover: var(--paper-raised);
+ --popover-foreground: var(--ink);
+ --primary: var(--moss);
+ --primary-foreground: var(--paper-raised);
+ --secondary: color-mix(in oklab, var(--sage) 35%, var(--paper-raised));
+ --secondary-foreground: var(--ink);
+ --muted: color-mix(in oklab, var(--sage) 20%, var(--paper-raised));
+ --muted-foreground: color-mix(in oklab, var(--ink) 62%, var(--moss) 38%);
+ --accent: var(--sand);
+ --accent-foreground: var(--ink);
+ --destructive: var(--clay);
+ --destructive-foreground: var(--paper-raised);
+ --border: var(--line);
+ --input: var(--line);
+ --ring: var(--sand);
+ --chart-1: var(--moss);
+ --chart-2: var(--sand);
+ --chart-3: var(--sage);
+ --chart-4: var(--clay);
+ --chart-5: var(--ink);
--radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.141 0.005 285.823);
- --sidebar-primary: oklch(0.21 0.006 285.885);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.967 0.001 286.375);
- --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
- --sidebar-border: oklch(0.92 0.004 286.32);
- --sidebar-ring: oklch(0.871 0.006 286.286);
-}
-
-.dark {
- --sea-ink: #d7ece8;
- --sea-ink-soft: #afcdc8;
- --lagoon: #60d7cf;
- --lagoon-deep: #8de5db;
- --palm: #6ec89a;
- --sand: #0f1a1e;
- --foam: #101d22;
- --surface: rgba(16, 30, 34, 0.8);
- --surface-strong: rgba(15, 27, 31, 0.92);
- --line: rgba(141, 229, 219, 0.18);
- --inset-glint: rgba(194, 247, 238, 0.14);
- --kicker: #b8efe5;
- --bg-base: #0a1418;
- --header-bg: rgba(10, 20, 24, 0.8);
- --chip-bg: rgba(13, 28, 32, 0.9);
- --chip-line: rgba(141, 229, 219, 0.24);
- --link-bg-hover: rgba(24, 44, 49, 0.8);
- --hero-a: rgba(96, 215, 207, 0.18);
- --hero-b: rgba(110, 200, 154, 0.12);
-
- --background: oklch(0.141 0.005 285.823);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.141 0.005 285.823);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.141 0.005 285.823);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.985 0 0);
- --primary-foreground: oklch(0.21 0.006 285.885);
- --secondary: oklch(0.274 0.006 286.033);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.274 0.006 286.033);
- --muted-foreground: oklch(0.705 0.015 286.067);
- --accent: oklch(0.274 0.006 286.033);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.396 0.141 25.723);
- --destructive-foreground: oklch(0.637 0.237 25.331);
- --border: oklch(0.274 0.006 286.033);
- --input: oklch(0.274 0.006 286.033);
- --ring: oklch(0.442 0.017 285.786);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.21 0.006 285.885);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.274 0.006 286.033);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(0.274 0.006 286.033);
- --sidebar-ring: oklch(0.442 0.017 285.786);
+ --sidebar: var(--paper-raised);
+ --sidebar-foreground: var(--ink);
+ --sidebar-primary: var(--moss);
+ --sidebar-primary-foreground: var(--paper-raised);
+ --sidebar-accent: var(--sand);
+ --sidebar-accent-foreground: var(--ink);
+ --sidebar-border: var(--line);
+ --sidebar-ring: var(--sand);
}
@theme inline {
--font-sans: 'Manrope', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -165,79 +106,49 @@ body,
body {
margin: 0;
- color: var(--sea-ink);
+ color: var(--ink);
font-family: var(--font-sans);
- background-color: var(--bg-base);
- background:
- radial-gradient(1100px 620px at -8% -10%, var(--hero-a), transparent 58%),
- radial-gradient(1050px 620px at 112% -12%, var(--hero-b), transparent 62%),
- radial-gradient(720px 380px at 50% 115%, rgba(79, 184, 178, 0.1), transparent 68%),
- linear-gradient(180deg, color-mix(in oklab, var(--sand) 68%, white) 0%, var(--foam) 44%, var(--bg-base) 100%);
+ background-color: var(--paper);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+/* A faint paper grain -- texture, not glow. */
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
- opacity: 0.28;
- background:
- radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.8), transparent 34%),
- radial-gradient(circle at 78% 26%, rgba(79, 184, 178, 0.2), transparent 42%),
- radial-gradient(circle at 42% 82%, rgba(47, 106, 74, 0.14), transparent 36%);
-}
-
-body::after {
- content: '';
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: -1;
- opacity: 0.14;
- background-image:
- linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
- linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
- background-size: 28px 28px;
- mask-image: radial-gradient(circle at 50% 30%, black, transparent 78%);
+ opacity: 0.5;
+ background-image: radial-gradient(color-mix(in oklab, var(--moss) 35%, transparent) 0.6px, transparent 0.6px);
+ background-size: 3px 3px;
}
a {
- color: var(--lagoon-deep);
- text-decoration-color: rgba(50, 143, 151, 0.4);
+ color: var(--moss);
+ text-decoration-color: color-mix(in oklab, var(--moss) 45%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
a:hover {
- color: #246f76;
+ color: var(--ink);
}
code {
+ font-family: var(--font-mono);
font-size: 0.9em;
border: 1px solid var(--line);
- background: color-mix(in oklab, var(--surface-strong) 82%, white 18%);
- border-radius: 7px;
+ background: var(--surface-strong);
+ border-radius: 6px;
padding: 2px 7px;
}
-pre code {
- border: 0;
- background: transparent;
- padding: 0;
- border-radius: 0;
- font-size: inherit;
- color: inherit;
-}
-
-.prose pre {
- border: 1px solid var(--line);
- border-radius: 12px;
- background: #1d2e45;
- color: #e8efff;
+.tabular-data {
+ font-family: var(--font-mono);
+ font-variant-numeric: tabular-nums;
}
.page-wrap {
@@ -249,33 +160,29 @@ pre code {
font-family: 'Fraunces', Georgia, serif;
}
-.island-shell {
+.island-shell,
+.feature-card {
border: 1px solid var(--line);
background: linear-gradient(165deg, var(--surface-strong), var(--surface));
box-shadow:
0 1px 0 var(--inset-glint) inset,
- 0 22px 44px rgba(30, 90, 72, 0.1),
- 0 6px 18px rgba(23, 58, 64, 0.08);
- backdrop-filter: blur(4px);
-}
-
-.feature-card {
- background: linear-gradient(165deg, color-mix(in oklab, var(--surface-strong) 93%, white 7%), var(--surface));
- box-shadow:
- 0 1px 0 var(--inset-glint) inset,
- 0 18px 34px rgba(30, 90, 72, 0.1),
- 0 4px 14px rgba(23, 58, 64, 0.06);
+ 0 14px 30px color-mix(in oklab, var(--ink) 10%, transparent),
+ 0 4px 12px color-mix(in oklab, var(--ink) 6%, transparent);
}
.feature-card:hover {
transform: translateY(-2px);
- border-color: color-mix(in oklab, var(--lagoon-deep) 35%, var(--line));
+ border-color: color-mix(in oklab, var(--sand) 45%, var(--line));
}
button,
.island-shell,
+.feature-card,
a {
- transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease,
+ transition:
+ background-color 180ms ease,
+ color 180ms ease,
+ border-color 180ms ease,
transform 180ms ease;
}
@@ -290,7 +197,7 @@ a {
.nav-link {
position: relative;
text-decoration: none;
- color: var(--sea-ink-soft);
+ color: color-mix(in oklab, var(--ink) 70%, var(--moss) 30%);
}
.nav-link::after {
@@ -302,13 +209,13 @@ a {
height: 2px;
transform: scaleX(0);
transform-origin: left;
- background: linear-gradient(90deg, var(--lagoon), #7ed3bf);
+ background: var(--sand);
transition: transform 170ms ease;
}
.nav-link:hover,
.nav-link.is-active {
- color: var(--sea-ink);
+ color: var(--ink);
}
.nav-link:hover::after,
@@ -333,7 +240,7 @@ a {
.site-footer {
border-top: 1px solid var(--line);
- background: color-mix(in oklab, var(--header-bg) 84%, transparent 16%);
+ background: var(--header-bg);
}
@layer base {