Files
OpsLog/frontend/src/style.css
T
rouggyandClaude Opus 5 543550c716 feat(sat): the map says what a pass is worth
An unselected satellite was a four-pixel dot with a one-pixel white edge, and
the basemap decides whether that is visible at all: a pale marker vanishes into
pale terrain, a grey one into a dark ocean, and the operator can switch between
the two in one session. So: a dark halo under a white ring around a larger dot.
Two rings, because no single colour reads on both.

The ones above the horizon now carry their name. Not all of them — a dozen
labels is a map nobody can read — but the two or three an operator is choosing
between right now, which is what saves hovering each grey dot in turn to find
them.

Hovering said "name · elevation · altitude", none of which decides anything.
What decides whether to reach for the radio is how long is left, how high it
will get and where to point, so the tooltip now carries the pass: elevation and
azimuth with its compass point, distance with an arrow for closing or receding,
rise and set with a countdown and a direction, and the peak. A bird already in
view shows its SET countdown instead of its rise — that is the number that
matters at that moment. No pass in the window says so, because a blank reads as
a fault. It costs no extra prediction: the pass list on screen is indexed by
name, and the first entry for a name is its next pass.

The tooltip also had to stop closing itself. The layer is rebuilt every five
seconds as the birds move, and a rebuilt marker is a new marker, so the detail
being read disappeared mid-sentence. The map now tracks the pointer and reopens
the tooltip of the dot it is still on — that one only, so nothing hangs open
once the mouse has moved away.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-09 11:04:26 +02:00

1285 lines
44 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* External @imports must come first per CSS spec. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
@import "tailwindcss";
@import "tw-animate-css";
/* ============================================================================
THEME SYSTEM
----------------------------------------------------------------------------
Every color is a semantic CSS variable declared per theme below. The
`@theme inline` block points Tailwind's color utilities (bg-*, text-*,
border-*, ring-*, …) directly at those variables, so switching the
`data-theme` attribute on <html> reskins the whole UI at runtime with no
rebuild. Four themes ship: two light-neutral surfaces and status families
for OK / warning / caution / danger / info that adapt to each surface.
Semantic status vocabulary (use these, NOT raw palette colors):
success — worked / confirmed / data / OK (was emerald)
warning — new-band / attention (was amber)
caution — new-mode / new-slot / lighter alert (was yellow)
danger — new / split / negative (was rose)
info — informational (was sky/blue)
destructive — hard errors / live REC / delete (was red)
Each family has: base (dots/bars/solid), -foreground (text on base),
-muted (soft pill fill), -muted-foreground (text on pill), -border.
========================================================================= */
/* ---- Theme 1: Warm light (default) — warm taupe & burnt orange ---------- */
:root,
[data-theme="light-warm"] {
--background: #e8dfc9; /* warm taupe — deeper than cream */
--foreground: #1c1917; /* stone-900 */
--card: #faf6ea; /* lifted off-white cream */
--card-foreground: #1c1917;
--popover: #faf6ea;
--popover-foreground: #1c1917;
--primary: #b8410c; /* burnt orange, slightly deeper */
--primary-foreground: #fff7ed;
--secondary: #ddd2b8;
--secondary-foreground: #1c1917;
--muted: #ddd2b8; /* toolbars / table headers */
--muted-foreground: #44403c; /* stone-700 — readable on muted */
--accent: #f8d6a9; /* warm amber-cream */
--accent-foreground: #7c2d12; /* orange-900 */
--destructive: #a51c1c;
--destructive-foreground: #ffffff;
--destructive-muted: #fef2f2;
--destructive-muted-foreground: #b91c1c;
--border: #c8b994; /* deeper warm beige border */
--input: #c8b994;
--ring: #d97706; /* amber-600 focus ring */
--success: #16a34a;
--success-foreground: #ffffff;
--success-muted: #dcfce7;
--success-muted-foreground: #166534;
--success-border: #86efac;
--warning: #d97706;
--warning-foreground: #ffffff;
--warning-muted: #fef3c7;
--warning-muted-foreground: #92400e;
--warning-border: #fcd34d;
--caution: #ca8a04;
--caution-foreground: #1c1917;
--caution-muted: #fef9c3;
--caution-muted-foreground: #854d0e;
--caution-border: #fde047;
--danger: #e11d48;
--danger-foreground: #ffffff;
--danger-muted: #ffe4e6;
--danger-muted-foreground: #9f1239;
--danger-border: #fda4af;
--info: #0284c7;
--info-foreground: #ffffff;
--info-muted: #e0f2fe;
--info-muted-foreground: #075985;
--info-border: #7dd3fc;
/* Band/mode matrix (BandSlotGrid) — original emerald/indigo/stone ramp. */
--mx-call-conf: #15803d; /* call confirmed */
--mx-call-work: #6ee7b7; /* call worked */
--mx-dx-conf: #3730a3; /* entity confirmed*/
--mx-dx-work: #a5b4fc; /* entity worked */
--mx-none: #e7e5e4; /* never worked */
/* Ring on the cell the operator is entering. Declared ONCE, on :root, and
deliberately not repeated per theme: it follows --warning, which every theme
already tunes to its own background. The token exists so the matrix ring can
be recoloured on its own without dragging every other warning in the app
with it (Appearance → matrix colours). */
--mx-cur: var(--warning);
/* The "worked with this callsign" dot. Declared ONCE, like --mx-cur: it is
drawn over every one of the five cell colours, so it follows the theme's own
foreground/background pair rather than a per-theme colour of its own. */
--mx-mark-work: var(--foreground);
--mx-mark-conf: var(--foreground);
--scrollbar-thumb: #b8a880;
--scrollbar-thumb-hover: #968455;
--card-shadow: 0 1px 2px rgba(28, 25, 23, 0.05), 0 0 0 1px rgba(28, 25, 23, 0.02);
color-scheme: light;
}
/* ---- Theme 1b: Cool light (slate/blue) — crisp daylight, neutral cool --- */
[data-theme="light-cool"] {
--background: #f4f6f8; /* cool blue-white */
--foreground: #0f172a; /* slate-900 */
--card: #ffffff;
--card-foreground: #0f172a;
--popover: #ffffff;
--popover-foreground: #0f172a;
--primary: #2563eb; /* blue-600 */
--primary-foreground: #ffffff;
--secondary: #e2e8f0; /* slate-200 */
--secondary-foreground: #0f172a;
--muted: #e6ebf1; /* toolbars / table headers */
--muted-foreground: #475569; /* slate-600 — readable on muted */
--accent: #dbeafe; /* blue-100 */
--accent-foreground: #1e3a8a; /* blue-900 */
--destructive: #dc2626;
--destructive-foreground: #ffffff;
--destructive-muted: #fef2f2;
--destructive-muted-foreground: #b91c1c;
--border: #d3dae2; /* cool slate border */
--input: #d3dae2;
--ring: #3b82f6; /* blue-500 focus ring */
--success: #16a34a;
--success-foreground: #ffffff;
--success-muted: #dcfce7;
--success-muted-foreground: #166534;
--success-border: #86efac;
--warning: #d97706;
--warning-foreground: #ffffff;
--warning-muted: #fef3c7;
--warning-muted-foreground: #92400e;
--warning-border: #fcd34d;
--caution: #ca8a04;
--caution-foreground: #1c1917;
--caution-muted: #fef9c3;
--caution-muted-foreground: #854d0e;
--caution-border: #fde047;
--danger: #e11d48;
--danger-foreground: #ffffff;
--danger-muted: #ffe4e6;
--danger-muted-foreground: #9f1239;
--danger-border: #fda4af;
--info: #0284c7;
--info-foreground: #ffffff;
--info-muted: #e0f2fe;
--info-muted-foreground: #075985;
--info-border: #7dd3fc;
/* Band/mode matrix (BandSlotGrid) — emerald/indigo ramp, cool neutral base. */
--mx-call-conf: #15803d;
--mx-call-work: #6ee7b7;
--mx-dx-conf: #3730a3;
--mx-dx-work: #a5b4fc;
--mx-none: #e2e8f0;
--scrollbar-thumb: #b6c2d1;
--scrollbar-thumb-hover: #8fa1b5;
--card-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 0 0 1px rgba(15, 23, 42, 0.02);
color-scheme: light;
}
/* ---- Theme 1c: Sage light (Nordic) — soft green-grey, low-fatigue ------- */
[data-theme="light-sage"] {
--background: #eef1ec; /* pale sage */
--foreground: #1a2420; /* deep pine-ink */
--card: #f8faf6; /* lifted off-white with a green cast */
--card-foreground: #1a2420;
--popover: #f8faf6;
--popover-foreground: #1a2420;
--primary: #2f855a; /* deep sage green */
--primary-foreground: #f0fdf4;
--secondary: #dde5db;
--secondary-foreground: #1a2420;
--muted: #dde5db; /* toolbars / table headers */
--muted-foreground: #445048; /* muted pine — readable on muted */
--accent: #d7e8d5; /* soft green */
--accent-foreground: #1e4634; /* forest-900 */
--destructive: #b91c1c;
--destructive-foreground: #ffffff;
--destructive-muted: #fef2f2;
--destructive-muted-foreground: #b91c1c;
--border: #cdd8ce; /* soft sage border */
--input: #cdd8ce;
--ring: #38a169; /* green focus ring */
--success: #16a34a;
--success-foreground: #ffffff;
--success-muted: #dcfce7;
--success-muted-foreground: #166534;
--success-border: #86efac;
--warning: #d97706;
--warning-foreground: #ffffff;
--warning-muted: #fef3c7;
--warning-muted-foreground: #92400e;
--warning-border: #fcd34d;
--caution: #ca8a04;
--caution-foreground: #1c1917;
--caution-muted: #fef9c3;
--caution-muted-foreground: #854d0e;
--caution-border: #fde047;
--danger: #e11d48;
--danger-foreground: #ffffff;
--danger-muted: #ffe4e6;
--danger-muted-foreground: #9f1239;
--danger-border: #fda4af;
--info: #0284c7;
--info-foreground: #ffffff;
--info-muted: #e0f2fe;
--info-muted-foreground: #075985;
--info-border: #7dd3fc;
/* Band/mode matrix (BandSlotGrid) — emerald/indigo ramp, sage neutral base. */
--mx-call-conf: #15803d;
--mx-call-work: #6ee7b7;
--mx-dx-conf: #3730a3;
--mx-dx-work: #a5b4fc;
--mx-none: #dde5db;
--scrollbar-thumb: #b3c2b3;
--scrollbar-thumb-hover: #91a591;
--card-shadow: 0 1px 2px rgba(26, 36, 32, 0.06), 0 0 0 1px rgba(26, 36, 32, 0.02);
color-scheme: light;
}
/* ---- Theme 1d: Dim slate — mid-tone twilight, between light and dark ---- */
[data-theme="dim-slate"] {
--background: #343b47; /* medium slate — light text, not deep dark */
--foreground: #eceef2;
--card: #3d4552; /* lifted panel */
--card-foreground: #eceef2;
--popover: #3d4552;
--popover-foreground: #eceef2;
--primary: #fb923c; /* orange-400 — brand warmth, pops on slate */
--primary-foreground: #241206;
--secondary: #454e5c;
--secondary-foreground: #eceef2;
--muted: #414a58; /* toolbars / table headers */
--muted-foreground: #b4bcc9; /* readable on muted */
--accent: #495468; /* hover / selection tint */
--accent-foreground: #dce3ef;
--destructive: #ef4444;
--destructive-foreground: #1a0808;
--destructive-muted: #3f1f1f;
--destructive-muted-foreground: #fca5a5;
--border: #4e5867;
--input: #4e5867;
--ring: #fdba74; /* orange-300 focus ring */
--success: #34d399;
--success-foreground: #06231a;
--success-muted: #1c3d34;
--success-muted-foreground: #6ee7b7;
--success-border: #2a5f4c;
--warning: #fbbf24;
--warning-foreground: #241a06;
--warning-muted: #41371a;
--warning-muted-foreground: #fcd34d;
--warning-border: #5e4d22;
--caution: #facc15;
--caution-foreground: #242006;
--caution-muted: #403b1a;
--caution-muted-foreground: #fde047;
--caution-border: #5b5222;
--danger: #fb7185;
--danger-foreground: #260a11;
--danger-muted: #421f28;
--danger-muted-foreground: #fda4af;
--danger-border: #63303c;
--info: #38bdf8;
--info-foreground: #061f2b;
--info-muted: #16384a;
--info-muted-foreground: #7dd3fc;
--info-border: #1d5670;
/* Matrix: bright confirmed, medium worked, slate not-worked (sits on dim). */
--mx-call-conf: #22c55e;
--mx-call-work: #2f7d55;
--mx-dx-conf: #6366f1;
--mx-dx-work: #3f3f8a;
--mx-none: #4a5262;
--scrollbar-thumb: #565f70;
--scrollbar-thumb-hover: #6b7588;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(236, 238, 242, 0.05);
color-scheme: dark;
}
/* ---- Theme 2: Warm dark (espresso) — same soul, night mode ------------- */
[data-theme="dark-warm"] {
--background: #221d18;
--foreground: #ede6d8;
--card: #2e2820;
--card-foreground: #ede6d8;
--popover: #2e2820;
--popover-foreground: #ede6d8;
--primary: #e07a2e; /* brighter burnt orange for dark */
--primary-foreground: #1a1512;
--secondary: #3a3229;
--secondary-foreground: #ede6d8;
--muted: #352e26;
--muted-foreground: #bcae9a;
--accent: #4a3a28;
--accent-foreground: #f8d6a9;
--destructive: #e5484d;
--destructive-foreground: #1a1512;
--destructive-muted: #3a1414;
--destructive-muted-foreground: #fca5a5;
--border: #473e33;
--input: #473e33;
--ring: #e8853a;
--success: #34d399;
--success-foreground: #0e2a20;
--success-muted: #123024;
--success-muted-foreground: #6ee7b7;
--success-border: #1e5540;
--warning: #fbbf24;
--warning-foreground: #2a1f08;
--warning-muted: #3a2e10;
--warning-muted-foreground: #fcd34d;
--warning-border: #5c4a18;
--caution: #facc15;
--caution-foreground: #2a2607;
--caution-muted: #38330f;
--caution-muted-foreground: #fde047;
--caution-border: #574f18;
--danger: #fb7185;
--danger-foreground: #2a0d13;
--danger-muted: #3a1720;
--danger-muted-foreground: #fda4af;
--danger-border: #5c2530;
--info: #38bdf8;
--info-foreground: #08222e;
--info-muted: #0c2a3a;
--info-muted-foreground: #7dd3fc;
--info-border: #164a5c;
/* Matrix: bright confirmed, medium-solid worked (clearly visible on dark),
warm-gray not-worked lifted off the card. */
--mx-call-conf: #22c55e;
--mx-call-work: #2f7d55;
--mx-dx-conf: #6366f1;
--mx-dx-work: #3f3f8a;
--mx-none: #453d33;
--scrollbar-thumb: #5c5044;
--scrollbar-thumb-hover: #7a6a58;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 240, 220, 0.04);
color-scheme: dark;
}
/* ---- Theme 3: Graphite dark — cool slate, technical look --------------- */
[data-theme="dark-graphite"] {
--background: #16181d;
--foreground: #e6e8ec;
--card: #1f232b;
--card-foreground: #e6e8ec;
--popover: #1f232b;
--popover-foreground: #e6e8ec;
--primary: #f97316; /* orange-500 pops on slate */
--primary-foreground: #0a0c10;
--secondary: #272c35;
--secondary-foreground: #e6e8ec;
--muted: #232830;
--muted-foreground: #9aa2b1;
--accent: #2a3240;
--accent-foreground: #cbd5e5;
--destructive: #ef4444;
--destructive-foreground: #0a0c10;
--destructive-muted: #331414;
--destructive-muted-foreground: #fca5a5;
--border: #2e343f;
--input: #2e343f;
--ring: #fb8c3c;
--success: #34d399;
--success-foreground: #06231a;
--success-muted: #0f2e26;
--success-muted-foreground: #6ee7b7;
--success-border: #17513f;
--warning: #fbbf24;
--warning-foreground: #241a06;
--warning-muted: #33290f;
--warning-muted-foreground: #fcd34d;
--warning-border: #4f3f16;
--caution: #facc15;
--caution-foreground: #242006;
--caution-muted: #322e0e;
--caution-muted-foreground: #fde047;
--caution-border: #4c4416;
--danger: #fb7185;
--danger-foreground: #260a11;
--danger-muted: #34141d;
--danger-muted-foreground: #fda4af;
--danger-border: #532430;
--info: #38bdf8;
--info-foreground: #061f2b;
--info-muted: #0b2938;
--info-muted-foreground: #7dd3fc;
--info-border: #124659;
/* Matrix: bright confirmed, medium-solid worked, cool-gray not-worked. */
--mx-call-conf: #22c55e;
--mx-call-work: #2f7d55;
--mx-dx-conf: #6366f1;
--mx-dx-work: #3f3f8a;
--mx-none: #333a45;
--scrollbar-thumb: #3a4150;
--scrollbar-thumb-hover: #4e5768;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(230, 232, 236, 0.04);
color-scheme: dark;
}
/* ---- Theme 4: High contrast — pure black / white, max legibility ------- */
[data-theme="high-contrast"] {
--background: #000000;
--foreground: #ffffff;
--card: #0d0d0d;
--card-foreground: #ffffff;
--popover: #0d0d0d;
--popover-foreground: #ffffff;
--primary: #ff7a1a;
--primary-foreground: #000000;
--secondary: #1a1a1a;
--secondary-foreground: #ffffff;
--muted: #141414;
--muted-foreground: #d4d4d4;
--accent: #262626;
--accent-foreground: #ffffff;
--destructive: #ff4d4d;
--destructive-foreground: #000000;
--destructive-muted: #2a0808;
--destructive-muted-foreground: #ff9999;
--border: #3f3f3f;
--input: #3f3f3f;
--ring: #ffab5e;
--success: #22e37a;
--success-foreground: #002313;
--success-muted: #06231a;
--success-muted-foreground: #6affb0;
--success-border: #0f6b4a;
--warning: #ffcc33;
--warning-foreground: #201900;
--warning-muted: #2a2205;
--warning-muted-foreground: #ffe08a;
--warning-border: #6b5510;
--caution: #ffe14d;
--caution-foreground: #201d00;
--caution-muted: #2a2705;
--caution-muted-foreground: #fff08a;
--caution-border: #6b6210;
--danger: #ff6b7d;
--danger-foreground: #23000a;
--danger-muted: #2a0a12;
--danger-muted-foreground: #ffb3bd;
--danger-border: #6b1f2c;
--info: #4dc4ff;
--info-foreground: #001a26;
--info-muted: #052330;
--info-muted-foreground: #a3e0ff;
--info-border: #0f5a6b;
/* Matrix: max-contrast ramp on pure black. */
--mx-call-conf: #2ee87f;
--mx-call-work: #17a85a;
--mx-dx-conf: #7d97ff;
--mx-dx-work: #3646b0;
--mx-none: #3a3a3a;
--scrollbar-thumb: #4a4a4a;
--scrollbar-thumb-hover: #666666;
--card-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
color-scheme: dark;
}
/* ---- Theme 5: Indigo — deep blue-violet, electric accent ----------------
The first seven themes are all neutral (beige, slate, graphite, black) with an
orange accent, so "another dark theme" only ever meant another grey. These
four carry a real hue in the SURFACES, not just in the accent, and each takes
a different primary — violet, cyan, pink, indigo — so they are told apart at a
glance in the picker.
Every one keeps the same contract: the semantic colours (success / warning /
caution / danger / info) must stay recognisable as themselves. A logger is
read for hours and "red = a problem" cannot become a decorative choice, so the
hue budget is spent on the surfaces and the primary, never on the meanings. */
[data-theme="dark-indigo"] {
--background: #0e0f1f;
--foreground: #e4e5f2;
--card: #181a30;
--card-foreground: #e4e5f2;
--popover: #181a30;
--popover-foreground: #e4e5f2;
--primary: #7c6cff; /* electric violet */
--primary-foreground: #0b0a1a;
--secondary: #222542;
--secondary-foreground: #e4e5f2;
--muted: #1e2038;
--muted-foreground: #9d9fbe;
--accent: #2a2d51;
--accent-foreground: #c7c9ee;
--destructive: #f2555a;
--destructive-foreground: #12030a;
--destructive-muted: #34131c;
--destructive-muted-foreground: #fca5a5;
--border: #2a2d4c;
--input: #2a2d4c;
--ring: #9b8dff;
--success: #34d399;
--success-foreground: #052018;
--success-muted: #0e2f27;
--success-muted-foreground: #6ee7b7;
--success-border: #165241;
--warning: #fbbf24;
--warning-foreground: #221803;
--warning-muted: #33280f;
--warning-muted-foreground: #fcd34d;
--warning-border: #4f3e15;
--caution: #facc15;
--caution-foreground: #221e04;
--caution-muted: #322d0e;
--caution-muted-foreground: #fde047;
--caution-border: #4b4315;
--danger: #fb7185;
--danger-foreground: #250912;
--danger-muted: #37151f;
--danger-muted-foreground: #fda4af;
--danger-border: #562533;
--info: #38bdf8;
--info-foreground: #051d29;
--info-muted: #0b2a3a;
--info-muted-foreground: #7dd3fc;
--info-border: #12475c;
/* The entity ramp moves to CYAN here: violet is the primary, and a violet
"entity confirmed" cell would read as a button. */
--mx-call-conf: #22c55e;
--mx-call-work: #2c7a52;
--mx-dx-conf: #a78bfa;
--mx-dx-work: #5b4a9e;
--mx-none: #2c2f4d;
--scrollbar-thumb: #383c63;
--scrollbar-thumb-hover: #4c5182;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(228, 229, 242, 0.05);
color-scheme: dark;
}
/* ---- Theme 6: Ocean — deep teal, cyan accent --------------------------- */
[data-theme="dark-teal"] {
--background: #061c21;
--foreground: #dcecee;
--card: #0d2c33;
--card-foreground: #dcecee;
--popover: #0d2c33;
--popover-foreground: #dcecee;
--primary: #22d3ee; /* cyan */
--primary-foreground: #041d24;
--secondary: #143b44;
--secondary-foreground: #dcecee;
--muted: #10333b;
--muted-foreground: #92b4ba;
--accent: #15424c;
--accent-foreground: #a5d8e0;
--destructive: #f2555a;
--destructive-foreground: #12030a;
--destructive-muted: #35161a;
--destructive-muted-foreground: #fca5a5;
--border: #1a4652;
--input: #1a4652;
--ring: #5fe3f5;
--success: #4ade80;
--success-foreground: #04220f;
--success-muted: #0e3020;
--success-muted-foreground: #86efac;
--success-border: #17593a;
--warning: #fbbf24;
--warning-foreground: #221803;
--warning-muted: #33280f;
--warning-muted-foreground: #fcd34d;
--warning-border: #4f3e15;
--caution: #facc15;
--caution-foreground: #221e04;
--caution-muted: #312d0e;
--caution-muted-foreground: #fde047;
--caution-border: #4a4315;
--danger: #fb7185;
--danger-foreground: #250912;
--danger-muted: #38161f;
--danger-muted-foreground: #fda4af;
--danger-border: #572634;
/* Info moves to a BLUE: a cyan "info" would be the primary colour again. */
--info: #7aa2f7;
--info-foreground: #04122e;
--info-muted: #16233f;
--info-muted-foreground: #a9c2fb;
--info-border: #2b3d66;
--mx-call-conf: #4ade80;
--mx-call-work: #2b7a4d;
--mx-dx-conf: #a78bfa;
--mx-dx-work: #5b4a9c;
--mx-none: #1c4550;
--scrollbar-thumb: #235a68;
--scrollbar-thumb-hover: #31788a;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(220, 236, 238, 0.05);
color-scheme: dark;
}
/* ---- Theme 7: Plum — aubergine, magenta accent ------------------------- */
[data-theme="dark-plum"] {
--background: #180f1e;
--foreground: #eee0f0;
--card: #251830;
--card-foreground: #eee0f0;
--popover: #251830;
--popover-foreground: #eee0f0;
--primary: #f472b6; /* magenta-pink */
--primary-foreground: #250518;
--secondary: #33203f;
--secondary-foreground: #eee0f0;
--muted: #2b1b36;
--muted-foreground: #b39dbd;
--accent: #3b2648;
--accent-foreground: #dcc2e6;
--destructive: #f2555a;
--destructive-foreground: #12030a;
--destructive-muted: #3a1620;
--destructive-muted-foreground: #fca5a5;
--border: #3a2547;
--input: #3a2547;
--ring: #f9a8d4;
--success: #34d399;
--success-foreground: #052018;
--success-muted: #10302a;
--success-muted-foreground: #6ee7b7;
--success-border: #1a5745;
--warning: #fbbf24;
--warning-foreground: #221803;
--warning-muted: #352a12;
--warning-muted-foreground: #fcd34d;
--warning-border: #524019;
--caution: #facc15;
--caution-foreground: #221e04;
--caution-muted: #342e12;
--caution-muted-foreground: #fde047;
--caution-border: #4e4519;
/* Danger goes RED here, not rose: a rose "danger" beside a pink primary is
indistinguishable, and this is the one colour that must never be missed. */
--danger: #f43f3f;
--danger-foreground: #2a0606;
--danger-muted: #3d1418;
--danger-muted-foreground: #fca5a5;
--danger-border: #5c2128;
--info: #38bdf8;
--info-foreground: #051d29;
--info-muted: #0f2b3a;
--info-muted-foreground: #7dd3fc;
--info-border: #16485d;
--mx-call-conf: #22c55e;
--mx-call-work: #2f7d55;
--mx-dx-conf: #60a5fa;
--mx-dx-work: #35538c;
--mx-none: #3a2647;
--scrollbar-thumb: #4a3159;
--scrollbar-thumb-hover: #644176;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(238, 224, 240, 0.05);
color-scheme: dark;
}
/* ---- Theme 8: Nordic light — crisp cool white, indigo accent ----------- */
[data-theme="light-nordic"] {
--background: #eef1f7;
--foreground: #1b2130;
--card: #ffffff;
--card-foreground: #1b2130;
--popover: #ffffff;
--popover-foreground: #1b2130;
--primary: #4f46e5; /* indigo */
--primary-foreground: #ffffff;
--secondary: #dfe4ee;
--secondary-foreground: #1b2130;
--muted: #e2e7f0;
--muted-foreground: #4a5468;
--accent: #dcdcfb;
--accent-foreground: #312a91;
--destructive: #b91c1c;
--destructive-foreground: #ffffff;
--destructive-muted: #fef2f2;
--destructive-muted-foreground: #b91c1c;
--border: #c9d1e0;
--input: #c9d1e0;
--ring: #6366f1;
--success: #15803d;
--success-foreground: #ffffff;
--success-muted: #dcfce7;
--success-muted-foreground: #14532d;
--success-border: #86efac;
--warning: #b45309;
--warning-foreground: #ffffff;
--warning-muted: #fef3c7;
--warning-muted-foreground: #92400e;
--warning-border: #fcd34d;
--caution: #a16207;
--caution-foreground: #ffffff;
--caution-muted: #fef9c3;
--caution-muted-foreground: #854d0e;
--caution-border: #fde047;
--danger: #dc2626;
--danger-foreground: #ffffff;
--danger-muted: #fee2e2;
--danger-muted-foreground: #991b1b;
--danger-border: #fca5a5;
--info: #0369a1;
--info-foreground: #ffffff;
--info-muted: #e0f2fe;
--info-muted-foreground: #075985;
--info-border: #7dd3fc;
/* Entity ramp shifts to TEAL: indigo is the primary here. */
--mx-call-conf: #15803d;
--mx-call-work: #86efac;
--mx-dx-conf: #0f766e;
--mx-dx-work: #99f6e4;
--mx-none: #dde3ec;
--scrollbar-thumb: #b6c0d2;
--scrollbar-thumb-hover: #93a0b8;
--card-shadow: 0 1px 2px rgba(27, 33, 48, 0.06), 0 0 0 1px rgba(27, 33, 48, 0.03);
color-scheme: light;
}
/* ---- Theme 1e: Sahara — Warm light, taken a step deeper --------------------
Warm light's cream is kept for panels and the entry strip (#faf6eb, the one
surface an operator stares at while typing a call), but the page behind them
and every toolbar, tab bar and table header go one shade further into the
sand. That was the whole request: same family, just a little darker, so the
cream panels lift clearly instead of blending into the page.
Ink is a deep warm brown rather than black: pure black on sand glares.
The accent moves from Warm light's burnt orange to ochre to keep the two
themes apart at a glance in the theme picker. ---------------------------- */
[data-theme="sahara"] {
--background: #e6ded0; /* parchment — the page */
--foreground: #3a3227; /* warm brown-ink */
--card: #f3eee2; /* panels, entry strip, log rows */
--card-foreground: #3a3227;
--popover: #f3eee2;
--popover-foreground: #3a3227;
--primary: #c4501e; /* terracotta — buttons and the frequency */
--primary-foreground: #fdf8f0;
--secondary: #ded5c4;
--secondary-foreground: #3a3227;
--muted: #ded5c4; /* toolbars / tab bar / table headers */
--muted-foreground: #6a5f4e; /* readable on both parchments */
--accent: #ecd9c0; /* warm highlight */
--accent-foreground: #7a3410;
--destructive: #b3401b;
--destructive-foreground: #ffffff;
--destructive-muted: #f7e4da;
--destructive-muted-foreground: #8f3315;
--border: #cdc2ae; /* one step down, so edges read without lines */
--input: #cdc2ae;
--ring: #c4501e; /* the accent, so focus matches the buttons */
--success: #15803d;
--success-foreground: #ffffff;
--success-muted: #dcf3e1;
--success-muted-foreground: #14532d;
--success-border: #86d99f;
--warning: #b45309;
--warning-foreground: #ffffff;
--warning-muted: #fbeacd;
--warning-muted-foreground: #7c3d05;
--warning-border: #e8b96a;
--caution: #a16207;
--caution-foreground: #1c1917;
--caution-muted: #f7edcb;
--caution-muted-foreground: #713f12;
--caution-border: #e0c069;
--danger: #be123c;
--danger-foreground: #ffffff;
--danger-muted: #fbe3e8;
--danger-muted-foreground: #9f1239;
--danger-border: #ee9aac;
--info: #0369a1;
--info-foreground: #ffffff;
--info-muted: #e0eef6;
--info-muted-foreground: #075985;
--info-border: #86c5e4;
/* Band/mode matrix — the app's blues kept, on a sand neutral so an empty slot
recedes into the grid instead of punching a hole in it. */
--mx-call-conf: #15803d;
--mx-call-work: #7fd6a4;
--mx-dx-conf: #3730a3;
--mx-dx-work: #a5b4fc;
--mx-none: #ded5c4;
--scrollbar-thumb: #c2b6a0;
--scrollbar-thumb-hover: #a5977e;
--card-shadow: 0 1px 2px rgba(58, 50, 39, 0.06), 0 0 0 1px rgba(58, 50, 39, 0.03);
color-scheme: light;
}
/* ---- Theme 13: DXHunter — its own slate and blue -----------------------
Ported so an operator running both side by side does not switch between two
colour worlds every time they look up. It is Tailwind's slate scale, which
is what DXHunter is built on: page at slate-900, panels at slate-800, rules
at slate-700, and blue-500 for the accent — counted across its sources, not
guessed from one panel: blue is 132 uses to violet's 25, and the violet is
the PSK Reporter panel alone. Active tab, focus border, primary button: all
blue-500. The status colours are DXHunter's too — emerald for good, amber
for attention, cyan for information, red for trouble — so a green number
means the same thing in both windows. */
[data-theme="dxhunter"] {
--background: #0f172a; /* slate-900 — the page */
--foreground: #e2e8f0; /* slate-200 */
--card: #1e293b; /* slate-800 — panels lift off the page */
--card-foreground: #e2e8f0;
--popover: #1e293b;
--popover-foreground: #e2e8f0;
--primary: #3b82f6; /* blue-500 — active tabs, focus, buttons */
--primary-foreground: #f8fafc;
--secondary: #273449;
--secondary-foreground: #e2e8f0;
--muted: #1c2941; /* toolbars / table headers */
--muted-foreground: #94a3b8; /* slate-400 — DXHunter's muted text */
--accent: #2c3b54; /* hover / selection tint */
--accent-foreground: #cbd5e1;
--destructive: #ef4444;
--destructive-foreground: #fef2f2;
--destructive-muted: #3a1518;
--destructive-muted-foreground: #fca5a5;
--border: #334155; /* slate-700 — every rule in DXHunter */
--input: #334155;
--ring: #60a5fa; /* blue-400 — its focus border */
--success: #34d399; /* emerald-400 — "online", confirmed */
--success-foreground: #04211a;
--success-muted: #0e3029;
--success-muted-foreground: #6ee7b7;
--success-border: #17564a;
--warning: #fbbf24; /* amber-400 */
--warning-foreground: #211803;
--warning-muted: #33280f;
--warning-muted-foreground: #fcd34d;
--warning-border: #4f3e15;
--caution: #facc15;
--caution-foreground: #211e04;
--caution-muted: #322d0e;
--caution-muted-foreground: #fde047;
--caution-border: #4b4315;
--danger: #f87171; /* red-400 — rose is barely used there */
--danger-foreground: #250912;
--danger-muted: #3a1621;
--danger-muted-foreground: #fda4af;
--danger-border: #5a2735;
--info: #22d3ee; /* cyan-400 — "heard near you" */
--info-foreground: #04212a;
--info-muted: #0c2f3b;
--info-muted-foreground: #67e8f9;
--info-border: #155e6e;
/* Blue is the primary, so the entity ramp goes VIOLET rather than reading
as a button — and violet is where DXHunter puts its own second accent. */
--mx-call-conf: #22c55e;
--mx-call-work: #2c7a52;
--mx-dx-conf: #a78bfa;
--mx-dx-work: #5b4a9e;
--mx-none: #334155;
--scrollbar-thumb: #334155; /* DXHunter's own scrollbar */
--scrollbar-thumb-hover: #475569;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(226, 232, 240, 0.05);
color-scheme: dark;
}
/* ---- Theme 14: DXHunter orange — the same slate, OpsLog's own accent --
DXHunter's slate, kept exactly — page, panels, rules, muted text, and its
status colours — with OpsLog's orange in place of its blue. For an operator
who wants the two windows to sit together without OpsLog losing the accent
it is recognised by. The entity ramp goes VIOLET here for the same reason as
in the blue version: it must not read as the accent. */
[data-theme="dxhunter-orange"] {
--background: #0f172a; /* slate-900 — the page */
--foreground: #e2e8f0; /* slate-200 */
--card: #1e293b; /* slate-800 — panels lift off the page */
--card-foreground: #e2e8f0;
--popover: #1e293b;
--popover-foreground: #e2e8f0;
--primary: #f97316; /* orange-500 — OpsLog's own accent */
--primary-foreground: #1c0a02;
--secondary: #273449;
--secondary-foreground: #e2e8f0;
--muted: #1c2941; /* toolbars / table headers */
--muted-foreground: #94a3b8; /* slate-400 — DXHunter's muted text */
--accent: #2c3b54; /* hover / selection tint */
--accent-foreground: #cbd5e1;
--destructive: #ef4444;
--destructive-foreground: #fef2f2;
--destructive-muted: #3a1518;
--destructive-muted-foreground: #fca5a5;
--border: #334155; /* slate-700 — every rule in DXHunter */
--input: #334155;
--ring: #fdba74; /* orange-300 focus ring */
--success: #34d399; /* emerald-400 — "online", confirmed */
--success-foreground: #04211a;
--success-muted: #0e3029;
--success-muted-foreground: #6ee7b7;
--success-border: #17564a;
--warning: #fbbf24; /* amber-400 */
--warning-foreground: #211803;
--warning-muted: #33280f;
--warning-muted-foreground: #fcd34d;
--warning-border: #4f3e15;
--caution: #facc15;
--caution-foreground: #211e04;
--caution-muted: #322d0e;
--caution-muted-foreground: #fde047;
--caution-border: #4b4315;
--danger: #f87171; /* red-400 — rose is barely used there */
--danger-foreground: #250912;
--danger-muted: #3a1621;
--danger-muted-foreground: #fda4af;
--danger-border: #5a2735;
--info: #22d3ee; /* cyan-400 — "heard near you" */
--info-foreground: #04212a;
--info-muted: #0c2f3b;
--info-muted-foreground: #67e8f9;
--info-border: #155e6e;
/* Blue is the primary, so the entity ramp goes VIOLET rather than reading
as a button — and violet is where DXHunter puts its own second accent. */
--mx-call-conf: #22c55e;
--mx-call-work: #2c7a52;
--mx-dx-conf: #a78bfa;
--mx-dx-work: #5b4a9e;
--mx-none: #334155;
--scrollbar-thumb: #334155; /* DXHunter's own scrollbar */
--scrollbar-thumb-hover: #475569;
--card-shadow: 0 1px 2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(226, 232, 240, 0.05);
color-scheme: dark;
}
/* ── Data-viz palette (Statistics dashboard) ────────────────────────────────
A VALIDATED categorical palette, not hand-picked: the slot ORDER is what makes
it colour-blind-safe (worst adjacent ΔE 24.2 light / 10.3 dark), so never
reorder, recycle, or generate a 9th hue — fold the tail into "Other" instead.
The dark column is the same eight hues re-stepped for a dark surface, NOT an
automatic inversion.
--chart-1 is the DEFAULT single-series hue: a chart with one series uses it for
every bar. Colour is for identity, never to re-encode a length the bar already
shows.
--chart-seq-* is a ONE-HUE ramp for magnitude (the hour×band heatmap). On light
themes it runs light→dark; on dark themes dark→light, so "near zero" always
recedes toward the surface instead of glowing. */
:root,
[data-theme="light-warm"],
[data-theme="light-cool"],
[data-theme="light-sage"],
[data-theme="light-nordic"],
[data-theme="sahara"] {
--chart-1: #2a78d6; /* blue — default single-series hue */
--chart-2: #1baf7a; /* aqua */
--chart-3: #eda100; /* yellow */
--chart-4: #008300; /* green */
--chart-5: #4a3aa7; /* violet */
--chart-6: #e34948; /* red */
--chart-7: #e87ba4; /* magenta */
--chart-8: #eb6834; /* orange */
--chart-seq-1: #cde2fb;
--chart-seq-2: #9ec5f4;
--chart-seq-3: #6da7ec;
--chart-seq-4: #3987e5;
--chart-seq-5: #256abf;
--chart-seq-6: #104281;
}
[data-theme="dim-slate"],
[data-theme="dark-warm"],
[data-theme="dark-graphite"],
[data-theme="high-contrast"],
[data-theme="dark-indigo"],
[data-theme="dark-teal"],
[data-theme="dark-plum"],
[data-theme="dxhunter"],
[data-theme="dxhunter-orange"] {
--chart-1: #3987e5;
--chart-2: #199e70;
--chart-3: #c98500;
--chart-4: #008300;
--chart-5: #9085e9;
--chart-6: #e66767;
--chart-7: #d55181;
--chart-8: #d95926;
/* Reversed: the lowest step must sink toward the dark surface. */
--chart-seq-1: #0d366b;
--chart-seq-2: #184f95;
--chart-seq-3: #256abf;
--chart-seq-4: #3987e5;
--chart-seq-5: #6da7ec;
--chart-seq-6: #9ec5f4;
}
/* Map Tailwind's color utilities onto the semantic vars above. `inline` makes
the generated utilities reference var(--…) directly, so overriding a var in
a [data-theme] block re-skins every utility at runtime. */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-destructive-muted: var(--destructive-muted);
--color-destructive-muted-foreground: var(--destructive-muted-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-success-muted: var(--success-muted);
--color-success-muted-foreground: var(--success-muted-foreground);
--color-success-border: var(--success-border);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-warning-muted: var(--warning-muted);
--color-warning-muted-foreground: var(--warning-muted-foreground);
--color-warning-border: var(--warning-border);
--color-caution: var(--caution);
--color-caution-foreground: var(--caution-foreground);
--color-caution-muted: var(--caution-muted);
--color-caution-muted-foreground: var(--caution-muted-foreground);
--color-caution-border: var(--caution-border);
--color-danger: var(--danger);
--color-danger-foreground: var(--danger-foreground);
--color-danger-muted: var(--danger-muted);
--color-danger-muted-foreground: var(--danger-muted-foreground);
--color-danger-border: var(--danger-border);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-info-muted: var(--info-muted);
--color-info-muted-foreground: var(--info-muted-foreground);
--color-info-border: var(--info-border);
--color-mx-call-conf: var(--mx-call-conf);
--color-mx-call-work: var(--mx-call-work);
--color-mx-dx-conf: var(--mx-dx-conf);
--color-mx-dx-work: var(--mx-dx-work);
--color-mx-none: var(--mx-none);
--color-mx-cur: var(--mx-cur);
--color-mx-mark-work: var(--mx-mark-work);
--color-mx-mark-conf: var(--mx-mark-conf);
--radius: 0.5rem;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Cascadia Mono', Consolas, 'Courier New', monospace;
}
@layer base {
* { @apply border-border; }
html, body, #root { height: 100%; }
/* No rubber-band, and no swipe-to-navigate.
Chromium lets a scroll that has reached the end pull the content a few
pixels and spring back, and a two-finger horizontal swipe means "go back a
page". Both are browser behaviours that have no meaning in a logbook: the
first makes a table wobble at the bottom of a list, and the second can
navigate the WebView away from the app entirely — a blank window and a
restart. Neither shows up with a mouse, which is why they went unnoticed;
they arrive with a precision touchpad or a touchscreen.
A no-op wherever the effects don't occur. */
html, body { overscroll-behavior: none; }
/* Root rem base. Tailwind sizing (text/heights/padding/gaps) is in rem, so
this is the single global density knob — shrinks the whole UI uniformly
while Leaflet maps (px-based) stay correct. Default browser base is 16px. */
html { font-size: 13px; }
body {
@apply bg-background text-foreground;
font-family: var(--font-sans);
font-size: 0.95rem;
line-height: 1.4;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
}
/* Subtle elevation on every Card-styled surface so cards visibly sit on
top of the background — paper-on-paper feel, tuned per theme. */
.bg-card {
box-shadow: var(--card-shadow);
}
/* Scrollbar tuned to each theme's surface */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 5px;
border: 2px solid var(--background);
}
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
/* ── Frameless window: drag region ──────────────────────────────────────────
The OS title bar is gone (main.go, Frameless), so the app header IS the title
bar and must be draggable. Everything interactive inside it opts OUT: a drag
region swallows clicks, and without this the menus and toolbar buttons would
move the window instead of doing their job. Opting out by ROLE rather than
listing each child keeps a future button working without anyone remembering
this rule. */
.app-dragregion {
--wails-draggable: drag;
}
.app-dragregion button,
.app-dragregion a,
.app-dragregion input,
.app-dragregion select,
.app-dragregion textarea,
.app-dragregion nav,
.app-dragregion [role='button'],
.app-dragregion [role='menu'],
.app-dragregion [data-no-drag] {
--wails-draggable: no-drag;
}
/* Native date inputs: the WebView draws its calendar glyph in near-black, which
disappears on the dark themes. Invert it there — the control itself is worth
keeping (OS calendar, locale date order, keyboard entry), only its icon needs
help. */
Every temporal input carries the same glyph, so the rule names them all:
datetime-local was left out at first and its icon stayed invisible. */
:is([data-theme='dim-slate'], [data-theme='dark-warm'], [data-theme='dark-graphite'], [data-theme='high-contrast'],
[data-theme='dark-indigo'], [data-theme='dark-teal'], [data-theme='dark-plum'])
:is(input[type='date'], input[type='datetime-local'], input[type='time'], input[type='month'], input[type='week'])::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(1.8);
opacity: 0.9;
}
/* Every scrollable pane keeps its overscroll to itself.
"contain" (not "none") on the inside: the pane still scrolls normally, it
simply stops passing the leftover motion up to its parent — which is how
scrolling a grid to its end used to tug the panel behind it. */
.overflow-auto,
.overflow-y-auto,
.overflow-x-auto,
.overflow-scroll {
overscroll-behavior: contain;
}
/* The ground beside the planet, on every map: Leaflet paints its container a
hard-coded light grey (#ddd), which reads as a broken tile against any
theme. The surround follows the theme's own card surface instead. */
.leaflet-container {
background: var(--card) !important;
}
/* Satellite map tooltips. Leaflet's own are a white box with a grey border —
fine on a street map, a bright rectangle on a dark one, and always the wrong
colours for whichever theme the operator chose. These follow the theme, and
are wide enough for a pass: AOS, LOS, elevation and range each on their own
line. */
.leaflet-tooltip.sat-tip {
background: var(--popover);
color: var(--popover-foreground);
border: 1px solid var(--border);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgb(0 0 0 / 0.35);
padding: 0.4rem 0.55rem;
font-size: 11px;
line-height: 1.45;
white-space: nowrap;
}
.leaflet-tooltip.sat-tip::before { border-top-color: var(--border); }
.sat-tip-name { font-weight: 600; font-size: 12px; margin-bottom: 0.2rem; }
.sat-tip-row { display: flex; justify-content: space-between; gap: 1.25rem; }
.sat-tip-row > span:first-child { color: var(--muted-foreground); }
.sat-tip-note { color: var(--muted-foreground); font-style: italic; }
/* The name beside a satellite that is up right now. A plain div marker and
not a Leaflet tooltip, because Leaflet keeps one tooltip per layer and the
hover detail is the one worth keeping. */
.sat-name-label {
pointer-events: none;
white-space: nowrap;
font-size: 10px;
font-weight: 600;
/* Painted twice — a dark halo under a light glyph — because the label sits on
satellite imagery, on a street map and on a dark ocean in the same session,
and no single colour is readable on all three. */
color: #fff;
text-shadow: 0 0 3px #000, 0 0 3px #000, 0 1px 2px #000;
}