Merge pull request 'fix(frontend): readability in both themes' (#188) from fix/ui-readability into main
Build & Deploy / Build & push images (push) Successful in 4m19s
Build & Deploy / Deploy Firestore rules & indexes (push) Successful in 34s
Build & Deploy / Deploy to VM (push) Successful in 1m51s
Build & Deploy / Report a failed deploy (push) Skipped

Reviewed-on: #188
This commit was merged in pull request #188.
This commit is contained in:
2026-09-27 16:20:54 -04:00
2 changed files with 35 additions and 15 deletions
+14
View File
@@ -67,6 +67,10 @@ html, body {
font-family: var(--font-mono), ui-monospace, monospace; font-family: var(--font-mono), ui-monospace, monospace;
} }
/* Dark mode: raw gray-600/700 text sits ~2.4:1 on the near-black page — below
* readable. Lift both to gray-500 (~4:1). */
.dark .text-gray-600, .dark .text-gray-700 { color: #6b7280; }
/* ── Light mode overrides ─────────────────────────────────────────────────── */ /* ── Light mode overrides ─────────────────────────────────────────────────── */
/* /*
* The app's components use hardcoded dark-palette Tailwind classes (bg-gray-9xx, * The app's components use hardcoded dark-palette Tailwind classes (bg-gray-9xx,
@@ -84,6 +88,7 @@ html:not(.dark) .bg-gray-900\/60 { background-color: rgba(255,255,255,0.85) !
html:not(.dark) .bg-gray-900\/50 { background-color: rgba(255,255,255,0.75) !important; } html:not(.dark) .bg-gray-900\/50 { background-color: rgba(255,255,255,0.75) !important; }
html:not(.dark) .bg-gray-900\/30 { background-color: rgba(255,255,255,0.50) !important; } html:not(.dark) .bg-gray-900\/30 { background-color: rgba(255,255,255,0.50) !important; }
html:not(.dark) .bg-gray-800 { background-color: #f1f5f9 !important; } html:not(.dark) .bg-gray-800 { background-color: #f1f5f9 !important; }
html:not(.dark) .bg-gray-800\/60 { background-color: rgba(226,232,240,0.70) !important; }
html:not(.dark) .bg-gray-800\/40 { background-color: rgba(241,245,249,0.60) !important; } html:not(.dark) .bg-gray-800\/40 { background-color: rgba(241,245,249,0.60) !important; }
html:not(.dark) .bg-gray-800\/30 { background-color: rgba(241,245,249,0.50) !important; } html:not(.dark) .bg-gray-800\/30 { background-color: rgba(241,245,249,0.50) !important; }
html:not(.dark) .bg-gray-700 { background-color: #e2e8f0 !important; } html:not(.dark) .bg-gray-700 { background-color: #e2e8f0 !important; }
@@ -91,16 +96,25 @@ html:not(.dark) .bg-gray-700 { background-color: #e2e8f0 !important; }
/* Borders */ /* Borders */
html:not(.dark) .border-gray-800 { border-color: #e2e8f0 !important; } html:not(.dark) .border-gray-800 { border-color: #e2e8f0 !important; }
html:not(.dark) .border-gray-700 { border-color: #cbd5e1 !important; } html:not(.dark) .border-gray-700 { border-color: #cbd5e1 !important; }
html:not(.dark) .border-gray-600 { border-color: #94a3b8 !important; }
html:not(.dark) .border-gray-800\/60 { border-color: #e2e8f0 !important; }
html:not(.dark) .divide-gray-800 > * + * { border-color: #e2e8f0 !important; } html:not(.dark) .divide-gray-800 > * + * { border-color: #e2e8f0 !important; }
/* Text */ /* Text */
html:not(.dark) .text-white { color: #0f172a !important; } html:not(.dark) .text-white { color: #0f172a !important; }
html:not(.dark) .text-gray-100 { color: #1e293b !important; } html:not(.dark) .text-gray-100 { color: #1e293b !important; }
html:not(.dark) .text-gray-200 { color: #1e293b !important; }
html:not(.dark) .text-gray-300 { color: #334155 !important; } html:not(.dark) .text-gray-300 { color: #334155 !important; }
html:not(.dark) .text-gray-400 { color: #475569 !important; } html:not(.dark) .text-gray-400 { color: #475569 !important; }
html:not(.dark) .text-gray-500 { color: #64748b !important; } html:not(.dark) .text-gray-500 { color: #64748b !important; }
html:not(.dark) .text-gray-600 { color: #94a3b8 !important; } html:not(.dark) .text-gray-600 { color: #94a3b8 !important; }
/* …except on saturated fills (primary/danger/success buttons), where the fill
* stays dark in both themes — remapping to navy made their labels unreadable. */
html:not(.dark) .text-white:is(.bg-accent, .bg-sev-major, .bg-sev-moderate,
.bg-indigo-500, .bg-indigo-600, .bg-indigo-700, .bg-red-600, .bg-red-700,
.bg-green-600, .bg-green-700, .bg-yellow-700, .bg-yellow-800, .bg-gray-600) { color: #ffffff !important; }
/* Hover states */ /* Hover states */
html:not(.dark) .hover\:bg-gray-900:hover { background-color: #f8fafc !important; } html:not(.dark) .hover\:bg-gray-900:hover { background-color: #f8fafc !important; }
html:not(.dark) .hover\:bg-gray-900\/50:hover { background-color: rgba(255,255,255,0.75) !important; } html:not(.dark) .hover\:bg-gray-900\/50:hover { background-color: rgba(255,255,255,0.75) !important; }
+21 -15
View File
@@ -1,5 +1,8 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
const tok = (name: string) =>
`color-mix(in srgb, var(--${name}) calc(<alpha-value> * 100%), transparent)`;
const config: Config = { const config: Config = {
content: [ content: [
"./app/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}",
@@ -15,24 +18,27 @@ const config: Config = {
// Semantic tokens — defined as CSS custom properties in app/globals.css on // Semantic tokens — defined as CSS custom properties in app/globals.css on
// :root (light) and .dark (dark). Components must use these names, never a // :root (light) and .dark (dark). Components must use these names, never a
// raw gray-9xx, so a theme is one variable block rather than an override sheet. // raw gray-9xx, so a theme is one variable block rather than an override sheet.
// Wrapped in color-mix so opacity modifiers (bg-surface/90, bg-accent/15)
// work — a bare var() hex gives Tailwind nothing to apply alpha to, and the
// class silently generates no rule (map overlays rendered see-through).
colors: { colors: {
page: "var(--page)", page: tok("page"),
surface: "var(--surface)", surface: tok("surface"),
raised: "var(--raised)", raised: tok("raised"),
line: "var(--line)", line: tok("line"),
"line-strong": "var(--line-strong)", "line-strong": tok("line-strong"),
ink: { ink: {
DEFAULT: "var(--ink)", DEFAULT: tok("ink"),
2: "var(--ink-2)", 2: tok("ink-2"),
muted: "var(--ink-muted)", muted: tok("ink-muted"),
}, },
accent: "var(--accent)", accent: tok("accent"),
"sev-moderate": "var(--sev-moderate)", "sev-moderate": tok("sev-moderate"),
"sev-major": "var(--sev-major)", "sev-major": tok("sev-major"),
"map-bg": "var(--map-bg)", "map-bg": tok("map-bg"),
"map-block": "var(--map-block)", "map-block": tok("map-block"),
"map-road": "var(--map-road)", "map-road": tok("map-road"),
"map-water": "var(--map-water)", "map-water": tok("map-water"),
}, },
// Marketing/product type scale — used by the (marketing) surface and // Marketing/product type scale — used by the (marketing) surface and
// settings shell so headings read as a deliberate hierarchy rather than // settings shell so headings read as a deliberate hierarchy rather than