feat(t-paliad-083): dark mode — auto + manual toggle, system-pref default (mAi/paliad#2)

Two-palette swap at :root and :root[data-theme="dark"]; FOUC-prevention
inline <script> in PWAHead reads paliad-theme + paliad-sidebar-pinned +
paliad-sidebar-width from localStorage before the stylesheet loads, so
the page paints in the persisted state from frame one. New theme.ts
client owns the runtime side: cycles auto → light → dark → auto, listens
to prefers-color-scheme while pref="auto", broadcasts change events to
the sidebar toggle so the sun/moon/auto icon stays in sync (incl. on
OS-level theme flips). Sidebar gains a sun/moon toggle below the lang
item with localized aria-label/tooltip describing the next click action.

Surface tokens introduced (--color-surface-{2,muted}, --color-input-bg,
--color-overlay-{faint,subtle,strong,modal}, --color-border-strong,
--shadow-{lg,xl}, --status-{red,amber,green,blue,neutral}-{bg,fg,...},
--tree-icon-{client,litigation,patent,case,project},
--sidebar-scrollbar-{thumb,...,width}); status pills, dashboard cards,
agenda urgency markers, frist-due-chip, akten-status-chip, termin-type
badges all read tokens or get a class-level dark override at the bottom
of global.css. Form inputs render on white in light mode (m: 2026-04-30)
and on a value below --color-surface in dark mode so the well still
reads as depressed below the card panel.

Sidebar scrollbar themed thin + cream-channel alpha with
scrollbar-gutter: stable so the collapsed icon column doesn't shift
when the nav overflows on tall (admin) layouts; .sidebar-icon width
shrinks by var(--sidebar-scrollbar-width) to keep icons centered in
the visible content area.

The pre-paint script also fixes the sidebar-pinned FOUC (maria's add):
sets <html class="sidebar-pinned"> from localStorage before paint, with
sidebar.ts mirroring the class on <html> on every pin toggle so the
new selector :root.sidebar-pinned .has-sidebar tracks the existing
.has-sidebar.sidebar-pinned (body) selector. width is also pre-applied
when within clamp.

Build: bun run build clean (1224 i18n keys, 36 pages).
Smoke: Playwright on /login in both modes — body bg/fg/cards/inputs
read from the right tokens, FOUC script lands in <head> before the
stylesheet, dark→light→auto cycle toggles via the sidebar button.
This commit is contained in:
m
2026-04-30 05:25:39 +02:00
parent 34e5ffe94b
commit fee6afdb14
8 changed files with 617 additions and 51 deletions

View File

@@ -1,14 +1,17 @@
// app.ts — universal client bundle injected on every page. Three jobs:
// app.ts — universal client bundle injected on every page. Four jobs:
// 1. Wire the BottomNav (was previously written but never bundled — m
// reproduced the broken [+] and Menü buttons in production).
// 2. Register the service worker so the site qualifies for PWA install.
// 3. Surface the install prompt (Chromium banner / iOS share-sheet hint).
// 4. Init the theme listener so the OS-level prefers-color-scheme change
// flips the page when the user's pref is "auto" (mAi/paliad#2).
//
// Per-page bundles still register their own behaviour; this script is
// orthogonal and only touches DOM nodes it owns.
import { initBottomNav } from "./bottom-nav";
import { initInstallPrompt } from "./pwa-install";
import { initTheme } from "./theme";
function registerServiceWorker(): void {
if (!("serviceWorker" in navigator)) return;
@@ -26,6 +29,7 @@ function registerServiceWorker(): void {
function boot(): void {
initBottomNav();
initInstallPrompt();
initTheme();
}
if (document.readyState === "loading") {

View File

@@ -43,6 +43,17 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.neuigkeiten": "Neuigkeiten",
"nav.soon.tooltip": "Bald verf\u00fcgbar",
// Theme toggle (mAi/paliad#2). The button cycles auto \u2192 light \u2192 dark
// \u2192 auto. The "toggle.<pref>" keys show the *current* pref next to
// the icon; the "cycle.<pref>" keys describe WHAT the next click will
// do (read by aria-label / tooltip).
"theme.toggle.auto": "Auto",
"theme.toggle.light": "Hell",
"theme.toggle.dark": "Dunkel",
"theme.toggle.cycle.auto": "Auf Hell-Modus wechseln",
"theme.toggle.cycle.light": "Auf Dunkel-Modus wechseln",
"theme.toggle.cycle.dark": "Auf Auto wechseln (System)",
// BottomNav (mobile)
"bottomnav.add": "Anlegen",
"bottomnav.menu": "Menü",
@@ -1353,6 +1364,14 @@ const translations: Record<Lang, Record<string, string>> = {
"nav.neuigkeiten": "What's New",
"nav.soon.tooltip": "Coming soon",
// Theme toggle (mAi/paliad#2)
"theme.toggle.auto": "Auto",
"theme.toggle.light": "Light",
"theme.toggle.dark": "Dark",
"theme.toggle.cycle.auto": "Switch to light theme",
"theme.toggle.cycle.light": "Switch to dark theme",
"theme.toggle.cycle.dark": "Switch to auto (system)",
// BottomNav (mobile)
"bottomnav.add": "New",
"bottomnav.menu": "Menu",

View File

@@ -3,6 +3,8 @@
import { initGlobalSearch } from "./search";
import { getChangelogSeen } from "./changelog-seen";
import { cycleTheme, getThemePref, onThemeChange, type ThemePref } from "./theme";
import { t } from "./i18n";
const PIN_KEY = "paliad-sidebar-pinned";
const LEGACY_PIN_KEY = "patholo-sidebar-pinned";
const WIDTH_KEY = "paliad-sidebar-width";
@@ -69,6 +71,7 @@ export function initSidebar() {
initGlobalSearch();
initChangelogBadge();
initAdminGroup();
initThemeToggle();
const sidebar = document.querySelector<HTMLElement>(".sidebar");
if (!sidebar) return;
initSidebarResize(sidebar);
@@ -83,10 +86,22 @@ export function initSidebar() {
return window.innerWidth < 1024;
}
// Restore pin state on desktop
// Restore pin state on desktop. We mirror the `sidebar-pinned` class on
// both <body> and <html>: the runtime path historically set it on body,
// but the pre-paint FOUC script in PWAHead.tsx can only reach <html>
// (body doesn't exist yet at that point). Keeping both in sync from
// every site that toggles pin state means the CSS selectors
// `.has-sidebar.sidebar-pinned` (body-rooted) and
// `:root.sidebar-pinned .has-sidebar` (html-rooted) never disagree.
if (localStorage.getItem(PIN_KEY) === "true" && !isMobile()) {
sidebar.classList.add("pinned");
document.body.classList.add("sidebar-pinned");
document.documentElement.classList.add("sidebar-pinned");
} else {
// Pre-paint script may have added it (e.g. last visit was wide enough
// and pinned) but current viewport is mobile — clear so the CSS rule
// doesn't fight the responsive @media block.
document.documentElement.classList.remove("sidebar-pinned");
}
// Desktop: hover expand with 150ms delay
@@ -117,11 +132,13 @@ export function initSidebar() {
if (wasPinned) {
sidebar.classList.remove("pinned");
document.body.classList.remove("sidebar-pinned");
document.documentElement.classList.remove("sidebar-pinned");
localStorage.setItem(PIN_KEY, "false");
} else {
sidebar.classList.add("pinned");
sidebar.classList.remove("expanded");
document.body.classList.add("sidebar-pinned");
document.documentElement.classList.add("sidebar-pinned");
localStorage.setItem(PIN_KEY, "true");
}
});
@@ -167,11 +184,13 @@ export function initSidebar() {
if (nowMobile) {
sidebar.classList.remove("expanded", "pinned");
document.body.classList.remove("sidebar-pinned");
document.documentElement.classList.remove("sidebar-pinned");
} else {
closeMobile();
if (localStorage.getItem(PIN_KEY) === "true") {
sidebar.classList.add("pinned");
document.body.classList.add("sidebar-pinned");
document.documentElement.classList.add("sidebar-pinned");
}
}
});
@@ -295,6 +314,64 @@ function initChangelogBadge(): void {
});
}
// initThemeToggle wires the sun/moon button at the bottom of the sidebar
// (mAi/paliad#2). The pre-paint inline script in PWAHead.tsx already set
// the data-theme attribute on <html>; this function only owns the post-
// hydration UI: cycling on click, swapping the icon/label to match the
// current preference, and re-rendering when the OS-level scheme flips
// while the user is on "auto".
const THEME_ICONS: Record<ThemePref, string> = {
auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor"/></svg>',
light: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg>',
dark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
};
const THEME_LABEL_KEYS: Record<ThemePref, "theme.toggle.auto" | "theme.toggle.light" | "theme.toggle.dark"> = {
auto: "theme.toggle.auto",
light: "theme.toggle.light",
dark: "theme.toggle.dark",
};
function initThemeToggle(): void {
const btn = document.getElementById("sidebar-theme-toggle") as HTMLButtonElement | null;
const icon = document.getElementById("sidebar-theme-icon") as HTMLElement | null;
const label = document.getElementById("sidebar-theme-label") as HTMLElement | null;
if (!btn || !icon || !label) return;
function render(): void {
const pref = getThemePref();
if (icon) icon.innerHTML = THEME_ICONS[pref];
if (label) {
const key = THEME_LABEL_KEYS[pref];
label.setAttribute("data-i18n", key);
label.textContent = t(key);
}
if (btn) {
btn.setAttribute("aria-label", t(key_for_aria(pref)));
btn.setAttribute("title", t(key_for_aria(pref)));
}
}
function key_for_aria(pref: ThemePref): "theme.toggle.cycle.auto" | "theme.toggle.cycle.light" | "theme.toggle.cycle.dark" {
// The aria-label describes WHAT happens on click, not the current
// state — assistive tech reads "Switch to dark theme" rather than
// just "Auto". Cycle order: auto → light → dark → auto.
if (pref === "auto") return "theme.toggle.cycle.auto"; // → light
if (pref === "light") return "theme.toggle.cycle.light"; // → dark
return "theme.toggle.cycle.dark"; // → auto
}
btn.addEventListener("click", () => {
cycleTheme();
});
// theme.ts notifies on user-cycle AND on OS-level prefers-color-scheme
// changes while the user is on "auto" — a single subscription covers
// both, so the icon/label always tracks the live preference.
onThemeChange(render);
render();
}
// initAdminGroup reveals the Admin section in the sidebar when the caller's
// /api/me lookup confirms global_role='global_admin'. The markup is in the
// DOM with display:none for everyone — flipping it on after the fetch lands

View File

@@ -0,0 +1,112 @@
// theme.ts — owns the runtime side of the dark-mode toggle.
//
// The pre-paint inline <script> in PWAHead.tsx already sets
// `<html data-theme="dark">` from localStorage[paliad-theme] before the
// stylesheet loads (FOUC-prevention, mAi/paliad#2). This module owns the
// post-hydration behaviour:
// - exposes the user-facing toggle helpers (cycleTheme, setThemePref)
// - re-applies the data-theme attribute when the user changes preference
// - listens for system prefers-color-scheme changes when pref === "auto"
//
// The pre-paint script and this module MUST share the same storage key
// and the same "auto + system → dark" decision logic. If you change one,
// change the other.
const STORAGE_KEY = "paliad-theme";
export type ThemePref = "auto" | "light" | "dark";
export function getThemePref(): ThemePref {
const v = localStorage.getItem(STORAGE_KEY);
if (v === "light" || v === "dark") return v;
// Anything else (null, "auto", legacy values) collapses to "auto" so a
// stale or hand-edited entry can't desync the toggle from the rest of
// the system.
return "auto";
}
export function setThemePref(pref: ThemePref): void {
if (pref === "auto") {
// Remove the key entirely on "auto" so the next read sees a clean
// null (matching the FOUC script's "no entry → auto" fallback). This
// also keeps localStorage tidy between users on shared machines.
localStorage.removeItem(STORAGE_KEY);
} else {
localStorage.setItem(STORAGE_KEY, pref);
}
applyTheme();
notifyChange();
}
function effectiveTheme(): "light" | "dark" {
const pref = getThemePref();
if (pref === "light" || pref === "dark") return pref;
return matchMedia && matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(): void {
const t = effectiveTheme();
if (t === "dark") {
document.documentElement.dataset.theme = "dark";
} else {
delete document.documentElement.dataset.theme;
}
}
const listeners: Array<() => void> = [];
function notifyChange(): void {
for (const cb of listeners) cb();
}
// onThemeChange returns an unsubscribe function. Used by Sidebar.tsx's
// theme-toggle button to re-render its icon + label without polling.
export function onThemeChange(cb: () => void): () => void {
listeners.push(cb);
return () => {
const i = listeners.indexOf(cb);
if (i !== -1) listeners.splice(i, 1);
};
}
// cycleTheme advances the preference: auto → light → dark → auto.
// Returns the new pref so callers can update aria-label / tooltip.
export function cycleTheme(): ThemePref {
const next: Record<ThemePref, ThemePref> = {
auto: "light",
light: "dark",
dark: "auto",
};
const n = next[getThemePref()];
setThemePref(n);
return n;
}
let initialized = false;
export function initTheme(): void {
if (initialized) return;
initialized = true;
// Re-apply on hydration. The pre-paint script already covered the
// initial frame, but matchMedia may not have been available there
// (very old browsers, deep iframes), so we settle the state again.
applyTheme();
// Listen for OS-level changes — only matters when pref === "auto",
// but the listener is cheap and the guard inside the handler keeps
// explicit user choices stable.
const mq = matchMedia("(prefers-color-scheme: dark)");
const handler = () => {
if (getThemePref() === "auto") {
applyTheme();
notifyChange();
}
};
if (typeof mq.addEventListener === "function") {
mq.addEventListener("change", handler);
} else if (typeof (mq as MediaQueryList & { addListener?: (h: () => void) => void }).addListener === "function") {
// Older Safari: the deprecated addListener still works.
(mq as MediaQueryList & { addListener: (h: () => void) => void }).addListener(handler);
}
}

View File

@@ -1,5 +1,27 @@
import { h, Fragment } from "../jsx";
// FOUC-prevention bootstrap (mAi/paliad#2 + maria's instruction 2026-04-30).
//
// Runs synchronously in <head> BEFORE the stylesheet starts loading. Its
// only job is to set classes/attributes on <html> that change first-frame
// layout, so the page paints in the user's persisted state instead of
// flickering from default → JS-applied state.
//
// Three pieces of state are restored:
// 1. paliad-theme — light / dark / auto-with-system-pref
// 2. paliad-sidebar-pinned — desktop-only padding-left swap
// 3. paliad-sidebar-width — user-resized rail width
//
// Body class for `sidebar-pinned` is owned by client/sidebar.ts post-
// hydration (we can't touch document.body from <head>). The CSS uses two
// selectors — `.has-sidebar.sidebar-pinned` (body class, runtime) and
// `:root.sidebar-pinned .has-sidebar` (html class, this script) — so
// either source pre-paints the correct padding.
//
// Storage keys and decision logic mirror frontend/src/client/theme.ts and
// client/sidebar.ts. If you change one, change the others.
const FOUC_BOOTSTRAP = `(function(){try{var d=document.documentElement,ls=localStorage;var p=ls.getItem("paliad-theme");if(p==="dark"||((!p||p==="auto")&&window.matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches))d.setAttribute("data-theme","dark");if(window.innerWidth>=1024&&ls.getItem("paliad-sidebar-pinned")==="true")d.classList.add("sidebar-pinned");var w=parseInt(ls.getItem("paliad-sidebar-width")||"",10);if(w>=180&&w<=480)d.style.setProperty("--sidebar-width",w+"px");}catch(e){}})();`;
// PWAHead emits the head fragment that turns Paliad into an installable PWA.
// Add it to every page's <head> alongside the existing viewport / theme-color
// metas. The <script src="/assets/app.js"> registers the service worker,
@@ -9,6 +31,7 @@ import { h, Fragment } from "../jsx";
export function PWAHead(): string {
return (
<Fragment>
<script dangerouslySetInnerHTML={{ __html: FOUC_BOOTSTRAP }} />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />

View File

@@ -25,6 +25,14 @@ const ICON_SPARKLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
const ICON_USERS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
const ICON_SHIELD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
const ICON_AUDIT_LOG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>';
// Theme-toggle icons. The button cycles auto → light → dark → auto, and
// the icon swaps to reflect the *current* preference (auto/light/dark)
// — not the eventual click target. SSR renders the auto variant; the
// runtime client (sidebar.ts) re-renders the matching icon on hydration
// after reading localStorage.
const ICON_THEME_AUTO = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor"/></svg>';
const ICON_THEME_LIGHT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg>';
const ICON_THEME_DARK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
interface SidebarProps {
currentPath: string;
@@ -172,6 +180,18 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
</span>
</span>
</div>
{/* Theme toggle (mAi/paliad#2). SSR renders the auto/system variant;
client/sidebar.ts swaps the icon + label to match the persisted
preference on hydration, and re-renders on every cycle. The
data-theme-toggle attribute is the click handle so the wiring
survives any future markup churn. */}
<button type="button" className="sidebar-item sidebar-theme-toggle"
id="sidebar-theme-toggle" data-theme-toggle="cycle"
aria-label="Theme">
<span className="sidebar-icon" id="sidebar-theme-icon"
dangerouslySetInnerHTML={{ __html: ICON_THEME_AUTO }} />
<span className="sidebar-label" id="sidebar-theme-label" data-i18n="theme.toggle.auto">Auto</span>
</button>
<a href="/logout" className="sidebar-item sidebar-logout">
<span className="sidebar-icon" dangerouslySetInnerHTML={{ __html: ICON_LOGOUT }} />
<span className="sidebar-label" data-i18n="nav.logout">Abmelden</span>

View File

@@ -1226,4 +1226,10 @@ export type I18nKey =
| "team.partner_unit.unassigned"
| "team.search.placeholder"
| "team.subtitle"
| "team.title";
| "team.title"
| "theme.toggle.auto"
| "theme.toggle.cycle.auto"
| "theme.toggle.cycle.dark"
| "theme.toggle.cycle.light"
| "theme.toggle.dark"
| "theme.toggle.light";

View File

@@ -19,25 +19,80 @@
--hlc-cyan-rgb: 159 227 217;
--hlc-cream-rgb: 238 229 225;
/* Surface + semantic tokens — re-pointed onto the brand palette. */
/* Surface + semantic tokens — re-pointed onto the brand palette.
Light mode is the default. Dark mode overrides every token below
in the :root[data-theme="dark"] block further down. Components
that read these tokens (instead of palette/hex literals) get the
theme swap for free. */
--color-bg: var(--hlc-cream);
--color-bg-subtle: #f7f3f0; /* slightly off-cream for table headers / soft surfaces */
--color-bg-lime-tint: rgb(var(--hlc-lime-rgb) / 0.10);
--color-surface: #ffffff; /* cards stay white for contrast */
--color-surface-2: #fafafa; /* slightly raised surface (alt rows, code blocks) */
--color-surface-muted: #f3f4f6; /* nested muted surfaces (chips, soft fills) */
/* Form inputs render on white in light mode (m: 2026-04-30 — the
cream `--color-bg` made inputs feel like background blocks, not
wells). In dark mode we drop slightly *below* the card surface
so the input still reads as a depressed well rather than merging
into the card panel. */
--color-input-bg: #ffffff;
--color-text: var(--hlc-midnight);
--color-text-muted: #5a6573;
--color-text-subtle: #6b7280; /* third-tier text (placeholders, helper) */
--color-accent: var(--hlc-lime);
--color-accent-light: #d8f78a; /* lighter lime for hover */
--color-accent-dark: var(--hlc-midnight); /* foreground on lime — WCAG AA */
/* Accent text colour. Lime fails WCAG on the cream/white BGs we use
in light mode, so accent emphasis in body copy is rendered in
midnight here. Dark-mode (mAi/paliad#2) flips this back to lime,
and the .sidebar scope below already keeps lime today since the
sidebar lives on a midnight BG. */
midnight here. Dark-mode flips this to lime (lime on midnight is
WCAG AA); the .sidebar scope below already keeps lime today since
the sidebar lives on a midnight BG, so it's a no-op there. */
--color-accent-fg: var(--hlc-midnight);
--color-border: #e1dcd6; /* derived from cream, neutral warm */
--color-border-strong: #d4d4d8; /* stronger border for tab/select boundaries */
--color-hero-bg: var(--hlc-midnight);
--color-hero-text: #ffffff;
/* Overlay tints for hover/stripe/divider — opacities tuned so they read
on cream backgrounds. Dark mode swaps to white-channel alphas. */
--color-overlay-faint: rgba(0, 0, 0, 0.04);
--color-overlay-subtle: rgba(0, 0, 0, 0.06);
--color-overlay-strong: rgba(0, 0, 0, 0.10);
--color-overlay-modal: rgba(0, 0, 0, 0.4); /* modal/drawer scrim */
/* Status palette — five buckets (red/amber/green/blue/neutral) shared
across dashboard cards, frist-due-chips, agenda urgency, termin
badges, login forms. Light values match the existing pastel-on-dark
Tailwind-100/700 pattern; dark values invert to a tinted alpha
backdrop with bright foreground for AA contrast on dark surfaces. */
--status-red-bg: #fee2e2;
--status-red-fg: #b91c1c;
--status-red-border: #fecaca;
--status-amber-bg: #fef3c7;
--status-amber-fg: #92400e;
--status-amber-fg-2: #b45309;
--status-amber-border: #fcd34d;
--status-green-bg: #dcfce7;
--status-green-fg: #166534;
--status-green-border: #bbf7d0;
--status-green-soft-bg: #ecfccb;
--status-green-soft-fg: #365314;
--status-blue-bg: #dbeafe;
--status-blue-fg: #1e40af;
--status-blue-fg-2: #2563eb;
--status-blue-soft-bg: #eef2ff;
--status-blue-soft-fg: #4338ca;
--status-neutral-bg: #f3f4f6;
--status-neutral-fg: #6b7280;
--status-neutral-fg-2: #475569;
--status-neutral-fg-3: #374151;
/* Project-tree icon palette — five identity colours by node type.
Light values are saturated-mid; dark values are brightened pastels
so they remain legible on midnight without losing identity. */
--tree-icon-client: #4338ca;
--tree-icon-litigation: #9d174d;
--tree-icon-patent: #075985;
--tree-icon-case: #92400e;
--tree-icon-project: #166534;
/* Sidebar — dark midnight surface with cream text + lime active.
Built from cream-channel alphas so contrast stays consistent
@@ -54,6 +109,16 @@
--radius: 8px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.15);
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.15);
/* Sidebar scrollbar — themed thin scrollbar so the auto-overflow
inside the nav reads as part of the dark sidebar surface and
doesn't clash visually with the page background. Cream-channel
alpha keeps it consistent in both light and dark mode (the
sidebar itself is always midnight). */
--sidebar-scrollbar-thumb: rgb(var(--hlc-cream-rgb) / 0.20);
--sidebar-scrollbar-thumb-hover: rgb(var(--hlc-cream-rgb) / 0.35);
--sidebar-scrollbar-width: 6px;
--max-width: 1080px;
--sidebar-collapsed: 64px;
--sidebar-expanded: 240px;
@@ -66,6 +131,88 @@
--bottom-nav-height: 56px;
}
/* Dark mode (mAi/paliad#2). Activated by the pre-paint inline script in
PWAHead.tsx setting <html data-theme="dark"> from localStorage[paliad-theme]
("dark" | "auto"+system-prefers-dark). The sidebar already lives on
midnight in light mode — its --sidebar-* tokens stay unchanged here, so
the sidebar reads identically across themes. The body palette flips to
midnight-bg + cream-fg, and accent text flips back to lime (passes WCAG
AA on midnight). Status pills, dashboard cards, and tree-icon colours
are tinted-alpha-on-dark for readability. */
:root[data-theme="dark"] {
--color-bg: var(--hlc-midnight);
--color-bg-subtle: #00304a; /* slightly raised for table headers */
--color-bg-lime-tint: rgb(var(--hlc-lime-rgb) / 0.12);
--color-surface: #0a3047; /* card surface — distinct from body */
--color-surface-2: #0d3a55; /* one step further raised */
--color-surface-muted: rgb(var(--hlc-cream-rgb) / 0.05);
/* Input wells render BELOW card surface so the depression reads. */
--color-input-bg: #00192a;
--color-text: var(--hlc-cream);
--color-text-muted: rgb(var(--hlc-cream-rgb) / 0.66);
--color-text-subtle: rgb(var(--hlc-cream-rgb) / 0.50);
--color-accent-light: #9bd926; /* darker lime for hover on dark */
--color-accent-dark: var(--hlc-midnight); /* foreground on lime stays midnight */
--color-accent-fg: var(--hlc-lime); /* lime text on midnight is AA */
--color-border: rgb(var(--hlc-cream-rgb) / 0.12);
--color-border-strong: rgb(var(--hlc-cream-rgb) / 0.22);
--color-hero-bg: #0a3047; /* hero panel one step lighter than bg */
--color-hero-text: var(--hlc-cream);
--color-overlay-faint: rgba(255, 255, 255, 0.04);
--color-overlay-subtle: rgba(255, 255, 255, 0.07);
--color-overlay-strong: rgba(255, 255, 255, 0.12);
--color-overlay-modal: rgba(0, 0, 0, 0.65);
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.6);
/* Status palette — alpha-tinted backdrops so the pill reads as a
coloured swatch rather than a saturated rectangle. Foregrounds are
the Tailwind 300-shades for AA contrast on the alpha bg. */
--status-red-bg: rgb(239 68 68 / 0.18);
--status-red-fg: #fca5a5;
--status-red-border: rgb(239 68 68 / 0.35);
--status-amber-bg: rgb(245 158 11 / 0.18);
--status-amber-fg: #fcd34d;
--status-amber-fg-2: #fbbf24;
--status-amber-border: rgb(245 158 11 / 0.35);
--status-green-bg: rgb(34 197 94 / 0.18);
--status-green-fg: #86efac;
--status-green-border: rgb(34 197 94 / 0.35);
--status-green-soft-bg: rgb(132 204 22 / 0.18);
--status-green-soft-fg: #bef264;
--status-blue-bg: rgb(59 130 246 / 0.18);
--status-blue-fg: #93c5fd;
--status-blue-fg-2: #60a5fa;
--status-blue-soft-bg: rgb(99 102 241 / 0.18);
--status-blue-soft-fg: #a5b4fc;
--status-neutral-bg: rgb(var(--hlc-cream-rgb) / 0.08);
--status-neutral-fg: rgb(var(--hlc-cream-rgb) / 0.66);
--status-neutral-fg-2: rgb(var(--hlc-cream-rgb) / 0.55);
--status-neutral-fg-3: rgb(var(--hlc-cream-rgb) / 0.78);
/* Tree-icon colours — brighter shades so they remain identifiable
on midnight without losing their colour identity. */
--tree-icon-client: #818cf8;
--tree-icon-litigation: #f472b6;
--tree-icon-patent: #38bdf8;
--tree-icon-case: #fbbf24;
--tree-icon-project: #86efac;
}
/* Smooth the body-background swap so theme toggles don't snap. Surface
and text colors are read by individual elements and inherit a similar
transition where they apply their own background/color shorthand. */
:root {
color-scheme: light;
}
:root[data-theme="dark"] {
color-scheme: dark;
}
*, *::before, *::after {
margin: 0;
padding: 0;
@@ -398,9 +545,9 @@ main {
}
.login-error {
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
background: var(--status-red-bg);
color: var(--status-red-fg);
border: 1px solid var(--status-red-border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
font-size: 0.85rem;
@@ -409,9 +556,9 @@ main {
}
.login-success {
background: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
background: var(--status-green-bg);
color: var(--status-green-fg);
border: 1px solid var(--status-green-border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
font-size: 0.85rem;
@@ -445,7 +592,7 @@ main {
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
color: var(--color-text);
background: var(--color-bg);
background: var(--color-input-bg);
}
.login-input:focus {
@@ -657,6 +804,31 @@ main {
overflow-x: hidden;
overflow-y: auto;
min-height: 0;
/* Themed thin scrollbar — cream-channel alpha so it reads as part of
the midnight sidebar surface in both light and dark mode. The
gutter is reserved (`stable`) so the sidebar's icon column doesn't
shift left when the scrollbar appears (admin nav can be tall
enough to overflow on shorter viewports). */
scrollbar-width: thin;
scrollbar-color: var(--sidebar-scrollbar-thumb) transparent;
scrollbar-gutter: stable;
}
/* WebKit fallback (Safari, older Chromium): explicit thumb styling. The
thumb width matches --sidebar-scrollbar-width so the visual gutter is
the same on Firefox (`scrollbar-width: thin`) and Webkit. */
.sidebar-nav::-webkit-scrollbar {
width: var(--sidebar-scrollbar-width);
}
.sidebar-nav::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: var(--sidebar-scrollbar-thumb);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: var(--sidebar-scrollbar-thumb-hover);
}
.sidebar-item {
@@ -699,7 +871,13 @@ main {
}
.sidebar-icon {
width: var(--sidebar-collapsed);
/* Width accounts for the reserved scrollbar gutter on .sidebar-nav so
icons stay visually centered inside the 64px collapsed rail rather
than drifting left when the scrollbar appears (or appearing left of
center even when it doesn't, since `scrollbar-gutter: stable`
always reserves the gutter). The other sidebar items (header,
bottom, hamburger) sit outside .sidebar-nav and are unaffected. */
width: calc(var(--sidebar-collapsed) - var(--sidebar-scrollbar-width));
display: flex;
align-items: center;
justify-content: center;
@@ -789,7 +967,14 @@ main {
transition: padding-left 150ms ease;
}
.has-sidebar.sidebar-pinned {
/* Two selectors apply the pinned padding because the pre-paint inline
script in PWAHead.tsx sets `<html class="sidebar-pinned">` (body
doesn't exist yet at that point), while the runtime sidebar.ts later
adds `sidebar-pinned` to `<body>` too. Either source is sufficient
and they don't conflict. Keeping both keeps the legacy DOM contract
intact (sidebar.ts continues to manage the body class). */
.has-sidebar.sidebar-pinned,
:root.sidebar-pinned .has-sidebar {
padding-left: var(--sidebar-width);
}
@@ -861,7 +1046,7 @@ main {
padding: 0.45rem 0.7rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
background: var(--color-input-bg);
color: var(--color-text);
outline: none;
cursor: pointer;
@@ -1590,23 +1775,23 @@ input[type="range"]::-moz-range-thumb {
}
.party-claimant {
background: #dbeafe;
color: #1e40af;
background: var(--status-blue-bg);
color: var(--status-blue-fg);
}
.party-defendant {
background: #fef3c7;
color: #92400e;
background: var(--status-amber-bg);
color: var(--status-amber-fg);
}
.party-court {
background: #f3e8ff;
color: #6b21a8;
background: var(--status-blue-soft-bg);
color: var(--status-blue-soft-fg);
}
.party-both {
background: #e5e7eb;
color: #374151;
background: var(--status-neutral-bg);
color: var(--status-neutral-fg-3);
}
.optional-badge {
@@ -1614,8 +1799,8 @@ input[type="range"]::-moz-range-thumb {
font-weight: 500;
padding: 0.05rem 0.4rem;
border-radius: 99px;
background: #fef3c7;
color: #92400e;
background: var(--status-amber-bg);
color: var(--status-amber-fg);
}
.timeline-rule {
@@ -1626,7 +1811,7 @@ input[type="range"]::-moz-range-thumb {
.timeline-adjusted {
font-size: 0.78rem;
color: #d97706;
color: var(--status-amber-fg-2);
margin-top: 0.2rem;
}
@@ -1670,7 +1855,7 @@ input[type="range"]::-moz-range-thumb {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--color-overlay-modal);
z-index: 35;
opacity: 0;
pointer-events: none;
@@ -1683,7 +1868,8 @@ input[type="range"]::-moz-range-thumb {
}
.has-sidebar,
.has-sidebar.sidebar-pinned {
.has-sidebar.sidebar-pinned,
:root.sidebar-pinned .has-sidebar {
padding-left: 0;
}
}
@@ -1903,7 +2089,7 @@ input[type="range"]::-moz-range-thumb {
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--color-overlay-modal);
display: flex;
align-items: center;
justify-content: center;
@@ -1914,7 +2100,7 @@ input[type="range"]::-moz-range-thumb {
.modal-card {
background: var(--color-surface);
border-radius: calc(var(--radius) * 1.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
box-shadow: var(--shadow-lg);
width: 100%;
max-width: 480px;
max-height: 90vh;
@@ -1972,7 +2158,12 @@ input[type="range"]::-moz-range-thumb {
font-size: 0.88rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
/* Form fields render on --color-input-bg, which is #ffffff in light
mode (m: 2026-04-30 — the cream `--color-bg` made inputs blur into
the body; white reads as a proper input well) and a value below
--color-surface in dark mode so the well is depressed below the
card panel rather than merging into it. */
background: var(--color-input-bg);
color: var(--color-text);
font-family: var(--font-sans);
outline: none;
@@ -2425,7 +2616,7 @@ input[type="range"]::-moz-range-thumb {
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
background: var(--color-overlay-modal);
display: flex;
align-items: center;
justify-content: center;
@@ -2439,7 +2630,7 @@ input[type="range"]::-moz-range-thumb {
padding: 2rem;
width: 100%;
max-width: 480px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
box-shadow: var(--shadow-xl);
}
.modal-content-sm {
@@ -2473,7 +2664,8 @@ input[type="range"]::-moz-range-thumb {
font-family: var(--font-sans);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
/* See .akten-form input rule above for the --color-input-bg rationale. */
background: var(--color-input-bg);
color: var(--color-text);
transition: border-color 0.15s ease;
}
@@ -3824,7 +4016,7 @@ input[type="range"]::-moz-range-thumb {
font-size: 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: #fff;
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
}
@@ -4093,11 +4285,11 @@ input[type="range"]::-moz-range-thumb {
height: 100%;
}
.projekt-tree-icon-client { color: #4338ca; }
.projekt-tree-icon-litigation { color: #9d174d; }
.projekt-tree-icon-patent { color: #075985; }
.projekt-tree-icon-case { color: #92400e; }
.projekt-tree-icon-project { color: #166534; }
.projekt-tree-icon-client { color: var(--tree-icon-client); }
.projekt-tree-icon-litigation { color: var(--tree-icon-litigation); }
.projekt-tree-icon-patent { color: var(--tree-icon-patent); }
.projekt-tree-icon-case { color: var(--tree-icon-case); }
.projekt-tree-icon-project { color: var(--tree-icon-project); }
.projekt-tree-title {
font-weight: 600;
@@ -4257,7 +4449,7 @@ input[type="range"]::-moz-range-thumb {
flex-wrap: wrap;
gap: 0.3rem;
align-items: center;
background: #fff;
background: var(--color-surface);
}
.akten-collab input {
@@ -4302,7 +4494,7 @@ input[type="range"]::-moz-range-thumb {
left: 0;
right: 0;
margin-top: 0.25rem;
background: #fff;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
@@ -4319,7 +4511,7 @@ input[type="range"]::-moz-range-thumb {
align-items: center;
padding: 0.5rem 0.75rem;
border: none;
background: #fff;
background: var(--color-surface);
text-align: left;
font-size: 0.85rem;
cursor: pointer;
@@ -4391,7 +4583,7 @@ input[type="range"]::-moz-range-thumb {
height: 36px;
border-radius: var(--radius);
border: 1px solid var(--color-border);
background: #fff;
background: var(--color-surface);
cursor: pointer;
display: inline-flex;
align-items: center;
@@ -6446,7 +6638,7 @@ input[type="range"]::-moz-range-thumb {
gap: 0.85rem;
align-items: flex-start;
padding: 0.9rem 1rem;
background: #fff;
background: var(--color-surface);
border: 1px solid var(--color-border, #e5e5ed);
border-radius: 12px;
transition: border-color 0.15s, box-shadow 0.15s;
@@ -6720,7 +6912,7 @@ dialog.quick-add-sheet[open] {
}
dialog.quick-add-sheet::backdrop {
background: rgba(0, 0, 0, 0.5);
background: var(--color-overlay-modal);
}
.quick-add-card {
@@ -6728,7 +6920,7 @@ dialog.quick-add-sheet::backdrop {
max-width: 480px;
background: var(--color-surface);
border-radius: 16px 16px 0 0;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
box-shadow: var(--shadow-md);
padding: 0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
@@ -7049,7 +7241,7 @@ dialog.quick-add-sheet::backdrop {
font-size: 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: #fff;
background: var(--color-surface);
color: var(--color-text);
}
@@ -7279,7 +7471,7 @@ dialog.quick-add-sheet::backdrop {
font-size: 0.9rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: #fff;
background: var(--color-surface);
color: var(--color-text);
}
@@ -7692,3 +7884,116 @@ dialog.quick-add-sheet::backdrop {
height: 480px;
}
}
/* ===================================================================
Dark mode — class-level overrides for sites that still hold their
own hardcoded colours (status pills, dashboard cards, agenda urgency,
termin types). The bulk of the swap is handled by the token redef
at :root[data-theme="dark"] earlier in this file; this block targets
the long-tail of class-scoped colour pairs that are too local to
warrant a full token. Keeping them in one section makes the dark-
mode palette greppable when tweaking contrast.
=================================================================== */
:root[data-theme="dark"] {
/* Surface elevations — tables/alt-rows/code-blocks read flat against
the body background otherwise. */
}
/* Soft amber surfaces (warning callouts, akten-unavailable, etc) */
:root[data-theme="dark"] .akten-unavailable {
background: var(--status-amber-bg);
color: var(--status-amber-fg);
}
/* Dashboard card counts + left-borders. Light mode keeps the saturated
Tailwind 700-shades; dark mode uses brightened pastels for AA. */
:root[data-theme="dark"] .dashboard-card-red .dashboard-card-count { color: var(--status-red-fg); }
:root[data-theme="dark"] .dashboard-card-red { border-left-color: var(--status-red-fg); }
:root[data-theme="dark"] .dashboard-card-amber .dashboard-card-count { color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .dashboard-card-amber { border-left-color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .dashboard-card-green .dashboard-card-count { color: var(--status-green-fg); }
:root[data-theme="dark"] .dashboard-card-done .dashboard-card-count { color: var(--status-neutral-fg-2); }
:root[data-theme="dark"] .dashboard-card-done { border-left-color: var(--status-neutral-fg-2); }
/* Dashboard urgency chips */
:root[data-theme="dark"] .dashboard-urgency-overdue { background: var(--status-red-bg); color: var(--status-red-fg); }
:root[data-theme="dark"] .dashboard-urgency-today { background: var(--status-amber-bg); color: var(--status-amber-fg); }
:root[data-theme="dark"] .dashboard-urgency-urgent { background: var(--status-amber-bg); color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .dashboard-urgency-soon { background: var(--status-green-soft-bg); color: var(--status-green-soft-fg); }
/* Dashboard termin dots */
:root[data-theme="dark"] .dashboard-termin-hearing { background: var(--status-red-fg); }
:root[data-theme="dark"] .dashboard-termin-meeting { background: var(--status-blue-fg-2); }
:root[data-theme="dark"] .dashboard-termin-deadline_hearing { background: var(--status-amber-fg-2); }
/* Termin type colour-codes (card border-left + dot) */
:root[data-theme="dark"] .termin-type-hearing { background: var(--status-red-fg); color: var(--status-red-fg); }
:root[data-theme="dark"] .termin-type-meeting { background: var(--status-blue-fg-2); color: var(--status-blue-fg-2); }
:root[data-theme="dark"] .termin-type-deadline_hearing { background: var(--status-amber-fg-2); color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .termin-type-default { background: var(--status-neutral-fg-2); color: var(--status-neutral-fg-2); }
/* Termin pill badges */
:root[data-theme="dark"] .termin-type-badge.termin-type-hearing { background: var(--status-red-bg); color: var(--status-red-fg); }
:root[data-theme="dark"] .termin-type-badge.termin-type-meeting { background: var(--status-blue-bg); color: var(--status-blue-fg-2); }
:root[data-theme="dark"] .termin-type-badge.termin-type-deadline_hearing { background: var(--status-amber-bg); color: var(--status-amber-fg); }
/* Termin card border-left colour cues */
:root[data-theme="dark"] .termin-card-today { border-left-color: var(--status-amber-fg-2); color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .termin-card-week { border-left-color: var(--status-blue-fg-2); color: var(--status-blue-fg-2); }
:root[data-theme="dark"] .termin-card-later { border-left-color: var(--status-neutral-fg-2); color: var(--status-neutral-fg-2); }
:root[data-theme="dark"] .termin-card-today .frist-summary-dot { background: var(--status-amber-fg-2); }
:root[data-theme="dark"] .termin-card-week .frist-summary-dot { background: var(--status-blue-fg-2); }
:root[data-theme="dark"] .termin-card-later .frist-summary-dot { background: var(--status-neutral-fg-2); }
/* Frist due-date chips */
:root[data-theme="dark"] .frist-due-chip.frist-urgency-overdue { background: var(--status-red-bg); color: var(--status-red-fg); }
:root[data-theme="dark"] .frist-due-chip.frist-urgency-soon { background: var(--status-amber-bg); color: var(--status-amber-fg); }
:root[data-theme="dark"] .frist-due-chip.frist-urgency-later { background: var(--status-green-bg); color: var(--status-green-fg); }
:root[data-theme="dark"] .frist-due-chip.frist-urgency-done { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
/* Akten-status chips (pending / cancelled / waived) */
:root[data-theme="dark"] .akten-status-chip.akten-status-pending { background: var(--status-amber-bg); color: var(--status-amber-fg); }
:root[data-theme="dark"] .akten-status-chip.akten-status-cancelled { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
:root[data-theme="dark"] .akten-status-chip.akten-status-waived { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
/* Agenda urgency markers */
:root[data-theme="dark"] .agenda-item-overdue .agenda-item-urgency,
:root[data-theme="dark"] .agenda-item-overdue .agenda-item-icon { background: var(--status-red-bg); color: var(--status-red-fg); }
:root[data-theme="dark"] .agenda-item-today .agenda-item-urgency,
:root[data-theme="dark"] .agenda-item-today .agenda-item-icon { background: var(--status-red-bg); color: var(--status-red-fg); }
:root[data-theme="dark"] .agenda-item-tomorrow .agenda-item-urgency,
:root[data-theme="dark"] .agenda-item-tomorrow .agenda-item-icon { background: var(--status-amber-bg); color: var(--status-amber-fg); }
:root[data-theme="dark"] .agenda-item-this_week .agenda-item-urgency,
:root[data-theme="dark"] .agenda-item-this_week .agenda-item-icon { background: var(--status-amber-bg); color: var(--status-amber-fg-2); }
:root[data-theme="dark"] .agenda-item-later .agenda-item-urgency,
:root[data-theme="dark"] .agenda-item-later .agenda-item-icon { background: var(--status-green-soft-bg); color: var(--status-green-soft-fg); }
/* Tab-style selection borders (gebuehren-tab, akten-tab, etc.) read on
the lime accent in both themes — no override needed. */
/* Code blocks / soft alt-row backgrounds — these specific Tailwind-grey
shades land too washed-out on midnight; pin them to surface-2 instead. */
:root[data-theme="dark"] .akten-table thead th,
:root[data-theme="dark"] .gebuehren-table th {
background: var(--color-bg-subtle);
}
/* Project breadcrumb pill (was background: #eef2ff; color: #4338ca;) */
:root[data-theme="dark"] .projekt-breadcrumb-pill,
:root[data-theme="dark"] .projekt-tree-pill {
background: var(--status-blue-soft-bg);
color: var(--status-blue-soft-fg);
}
/* Generic .frist-card hover/alt rows — neutralize the rgba(0,0,0,...)
stripes and dividers so the dark surface doesn't get a black wash. */
:root[data-theme="dark"] .frist-card:hover,
:root[data-theme="dark"] .akten-table tbody tr:hover {
background: var(--color-bg-lime-tint);
}
/* Light-mode placeholder scrollbar tinting — already cream-channel alpha
on the sidebar (which is midnight in both themes). On the body in dark
mode, the browser-default scrollbar is dark via color-scheme, so no
per-element override is needed here. */