Explorar el Código

Phase 16: dark-mode toggle + light-mode contrast cleanup

Two things were slightly off in the default theme:

1. Both body and table-header bands used bg-slate-50 — the thead
   band blended into the page tint, so tables looked like they had
   no header at all until you clicked one of the sort columns.
2. Users projecting the Phase 15 beamer view in a dim conference
   room wanted a dark palette. Today there was no switch, no CSS
   scoping, and Tailwind's dark variants were disabled.

This phase addresses both. In light mode the body background
bumps one shade cooler to bg-slate-100 so the slate-50 header
bands now read as a distinct lighter strip. Dark mode is a
manual, browser-local toggle persisted in localStorage['sp:theme'];
no system-preference auto-detect, no server round-trip.

Strict CSP stays intact — theme-init.js is a standard
<script src> under script-src 'self'. No new external hosts.

tailwind.config.js:
- darkMode: 'class' enables dark: variants only when
  <html class="dark"> is set. Content globs already cover
  views/**/*.php and public/assets/js/**/*.js, so every new
  dark:bg-slate-... class we added ends up in the compiled
  /assets/css/app.css on the next image rebuild.

public/assets/js/theme-init.js (new, 8 lines):
- Synchronous, no defer/async — loaded from <head> before the
  stylesheet so no FOUC flash between bright and dark.
- try/catch around localStorage.getItem so private-window
  denials silently fall through to light.
- The only logic: if localStorage['sp:theme'] === 'dark', add
  'dark' to <html>. That's it.

public/assets/js/app.js:
- Appends a third IIFE (~17 lines) wiring the [data-theme-toggle]
  button. On click: toggles 'dark' on <html>, writes
  localStorage['sp:theme'], updates the [data-theme-label] text.
  On boot: stamps the label from the current class state so the
  hamburger opens with the right word showing. Writes wrapped in
  try/catch so private-window denials degrade gracefully (toggle
  still works for the session, just doesn't persist).

views/layout.php:
- <script src="/assets/js/theme-init.js"> added to <head> before
  the stylesheet link. No defer/async — intentional.
- Body: bg-slate-50 -> bg-slate-100 (the user's explicit ask).
  Added dark:bg-slate-900 dark:text-slate-100.
- Hamburger dropdown: new Theme row inserted above the Sign-out
  divider. Visible to both admins and non-admins (it's a personal
  preference, not an admin action). To keep the divider logic
  tidy for non-admins — who previously saw no <hr> — the Theme
  row always renders; the <hr> now always sits between it and
  Sign out. dark: siblings added to every bg-/text-/border-
  class on the header, menu panel, admin badge, and links.
- Sign-out form block is untouched — still a native POST with
  the _csrf hidden input.

views/sprints/present.php:
- Mirror treatment: <script src="/assets/js/theme-init.js"> in
  its own <head>, before the stylesheet. body picks up
  dark:bg-slate-900 dark:text-slate-100. beamer-root header,
  task-section card, task-list toolbar, table thead/tbody,
  inputs, owner/columns dropdowns, and the empty-state banners
  all grow dark: siblings matching the palette in the plan.

views/home.php, views/auth/local.php, views/workers/index.php,
views/users/index.php, views/sprints/new.php,
views/sprints/settings.php, views/sprints/show.php,
views/audit/index.php:
- Systematic sweep. For every hand-picked light colour
  (bg-white, bg-slate-50/100/200, text-slate-300..700 used as
  text, text-red-600/700, bg-green-/red-/amber-100 status chips,
  border-slate-200/300, divide-slate-100), added a co-located
  dark: sibling from the palette spec:
    * body: dark:bg-slate-900
    * cards / panels: dark:bg-slate-800
    * table headers: dark:bg-slate-700
    * borders: dark:border-slate-700 (or -slate-600 for inputs)
    * primary text: dark:text-slate-100
    * secondary text: dark:text-slate-400
    * inputs: dark:bg-slate-800 dark:border-slate-600
              dark:text-slate-100 dark:focus:ring-slate-500
    * links: dark:text-blue-400 dark:hover:text-blue-300
    * success flash: dark:bg-green-900 dark:text-green-200
                     dark:border-green-800
    * error flash:   dark:bg-red-900 dark:text-red-200
                     dark:border-red-800
    * admin badge:   dark:bg-amber-900 dark:text-amber-200
    * weekday dots (show.php Arbeitstage row):
        active dark:bg-green-400, off dark:bg-slate-600
    * capacity "available" red (show.php):
        dark:text-red-400
    * audit action chips (CREATE/UPDATE/DELETE/LOGIN/...):
        dark:bg-{green|blue|red|slate|amber|purple}-900 +
        dark:text-{green|blue|red|slate|amber|purple}-200
- No structural changes — only class attributes grow.

ACCEPTANCE.md:
- New "Phase 16 — Dark mode + light contrast" section with the
  six scenarios from the plan (light-band separation, toggle
  flip, reload persistence + no-FOUC with Network throttling,
  present-view inherits dark, admin-pages sweep for contrast,
  private-window localStorage denial fallback).

Tests. Zero PHPUnit — same pattern as Phases 10, 13, 14, 15.
CSS class additions plus ~25 lines of vanilla JS don't have a
meaningful unit surface the existing harness can reach.

phpunit: 88 / 208, OK.
node --check public/assets/js/app.js: clean.
theme-init.js is 8 lines of vanilla JS (try/catch + classList.add),
trivially valid; the sandboxed shell in this session repeatedly
blocked `node --check` specifically against that path, so the
dedicated invocation is skipped.

Not exercised in a running container — manual acceptance is on
the human via the new ACCEPTANCE.md section. The UI change is
class-only, so the verification is visual by design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa hace 2 semanas
padre
commit
94b2841599

+ 83 - 0
ACCEPTANCE.md

@@ -283,3 +283,86 @@ the **Present** button in the sprint header (opens in a new tab).
      (20+ workers), the browser console shows
      (20+ workers), the browser console shows
      `[sprint-planner] beamer: table still overflows …` and
      `[sprint-planner] beamer: table still overflows …` and
      horizontal scroll is enabled — the page does not spin.
      horizontal scroll is enabled — the page does not spin.
+
+## Phase 16 — Dark mode + light contrast
+
+Runs with any signed-in user. The new Theme row lives in the
+hamburger menu (visible for admins and non-admins alike,
+sitting above the `<hr>` that separates it from Sign out).
+Toggle state persists per browser in `localStorage['sp:theme']`
+and is applied synchronously by `/assets/js/theme-init.js`
+before the stylesheet resolves — zero FOUC.
+
+1. **Fresh page load in light mode: body is cooler than the
+   table headers (visible band separation).**
+   - Open http://localhost:8080 in a fresh browser profile (or
+     clear `sp:theme` from localStorage first).
+   - Expected: body background is `bg-slate-100`, table
+     `<thead>` bands stay at `bg-slate-50`. Looking at the
+     home page Sprints table, the header band now reads as a
+     distinctly lighter strip above rows, no longer blending
+     into the page tint. Cards (`bg-white`) still pop above
+     slate-100 as before.
+
+2. **Click Theme in the hamburger menu → whole app flips to
+   dark; label reads "Dark".**
+   - Sign in. Click the hamburger. Panel shows (admin:
+     Workers / Users / Audit log / **Theme (Light)** / `<hr>` /
+     Sign out; non-admin: **Theme (Light)** / `<hr>` / Sign out).
+   - Click **Theme**.
+   - Expected: `<html class="dark">` is set. Body goes
+     `dark:bg-slate-900`, header and cards go
+     `dark:bg-slate-800`, table header bands go
+     `dark:bg-slate-700`, borders go `dark:border-slate-700`.
+     Primary text reads `dark:text-slate-100`. Re-open the
+     menu — the right-hand label now reads **Dark**. Click
+     again to flip back.
+
+3. **Reload the page → dark persists, no flash of light
+   before styles apply.**
+   - With the app in dark mode, reload (F5). Watch the first
+     paint carefully — in devtools, enable Network throttling
+     ("Slow 3G") and reload so you can see the first paint
+     before CSS applies.
+   - Expected: the background is dark immediately (theme-init.js
+     is a synchronous `<script src>` in `<head>` before the
+     stylesheet, so the `dark` class is on `<html>` the first
+     time the stylesheet resolves). No white flash.
+
+4. **Open `/sprints/{id}/present` in a new tab → picks up dark
+   too (theme-init.js in its head).**
+   - With dark mode active in the main tab, open a sprint and
+     click **Present** (new tab).
+   - Expected: the presentation view loads already in dark mode
+     — body `dark:bg-slate-900`, top bar
+     `dark:bg-slate-800`, task table header
+     `dark:bg-slate-700`, inputs `dark:bg-slate-800
+     dark:text-slate-100`. Verifies the separate
+     `<!doctype html>` emitted by `views/sprints/present.php`
+     includes the same theme-init.js tag in its `<head>`.
+
+5. **Back to light mode, open Workers / Users / Audit /
+   Settings pages: no stray white-on-white or unreadable text
+   anywhere.**
+   - Toggle back to light mode. As admin, walk through:
+     `/workers`, `/users`, `/audit`, `/sprints/new`,
+     `/sprints/{id}`, `/sprints/{id}/settings`,
+     `/sprints/{id}/present`.
+   - Expected: every page reads cleanly in light mode (slate-100
+     body, slate-50 header bands, white cards). Toggle to dark
+     and do the same sweep — every flash chip (green/red/
+     amber), admin badge, focus ring, and capacity-red cell
+     still has legible contrast on the slate-800/900 surfaces.
+
+6. **Private-window (localStorage denied) → defaults to light,
+   toggle no-ops without throwing.**
+   - Open a private / incognito window that blocks localStorage
+     (or disable storage for the origin in devtools → Application
+     → Storage → Local Storage).
+   - Expected: the app loads in light mode (theme-init.js's
+     try/catch silently swallows the read denial). Opening the
+     hamburger and clicking **Theme** still flips the class on
+     `<html>` for the current page (in-memory), label updates
+     to "Dark"/"Light"; the write to localStorage is caught by
+     try/catch and the page does not throw. Reloading resets to
+     light (no persistence possible).

+ 25 - 0
public/assets/js/app.js

@@ -77,3 +77,28 @@
         if (ev.target.closest('[role="menuitem"]')) { setOpen(false); }
         if (ev.target.closest('[role="menuitem"]')) { setOpen(false); }
     });
     });
 })();
 })();
+
+/**
+ * Phase 16: dark-mode toggle. theme-init.js in <head> already applied the
+ * 'dark' class from localStorage before stylesheet resolution, so this
+ * handler only cares about the on-click flip + the menu label's text.
+ * localStorage writes are try/catch'd so private-window denials degrade
+ * gracefully (toggle still works for the session, just doesn't persist).
+ */
+(function () {
+    'use strict';
+    const btn   = document.querySelector('[data-theme-toggle]');
+    const label = document.querySelector('[data-theme-label]');
+    if (!btn || !label) { return; }
+
+    function stamp() {
+        label.textContent = document.documentElement.classList.contains('dark') ? 'Dark' : 'Light';
+    }
+    stamp();
+
+    btn.addEventListener('click', function () {
+        const nowDark = document.documentElement.classList.toggle('dark');
+        try { localStorage.setItem('sp:theme', nowDark ? 'dark' : 'light'); } catch (e) { /* ignore */ }
+        stamp();
+    });
+})();

+ 8 - 0
public/assets/js/theme-init.js

@@ -0,0 +1,8 @@
+/* Phase 16: sync theme apply before stylesheet load — avoids FOUC.
+   Loaded without defer/async from <head>. Reads sp:theme; dark → add class.
+   Wrapped in try/catch so private-window localStorage denials no-op. */
+try {
+    if (localStorage.getItem('sp:theme') === 'dark') {
+        document.documentElement.classList.add('dark');
+    }
+} catch (e) { /* localStorage denied — stay light. */ }

+ 1 - 0
tailwind.config.js

@@ -1,5 +1,6 @@
 /** @type {import('tailwindcss').Config} */
 /** @type {import('tailwindcss').Config} */
 module.exports = {
 module.exports = {
+    darkMode: 'class',
     content: [
     content: [
         './views/**/*.php',
         './views/**/*.php',
         './src/**/*.php',
         './src/**/*.php',

+ 41 - 41
views/audit/index.php

@@ -34,7 +34,7 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
     <header class="flex items-end justify-between gap-4">
     <header class="flex items-end justify-between gap-4">
         <div>
         <div>
             <h1 class="text-2xl font-semibold tracking-tight">Audit log</h1>
             <h1 class="text-2xl font-semibold tracking-tight">Audit log</h1>
-            <p class="text-slate-600 text-sm mt-1">
+            <p class="text-slate-600 text-sm mt-1 dark:text-slate-400">
                 <?= (int) $total ?> matching row<?= $total === 1 ? '' : 's' ?>
                 <?= (int) $total ?> matching row<?= $total === 1 ? '' : 's' ?>
                 · page <?= (int) $page ?> / <?= (int) $pages ?>
                 · page <?= (int) $page ?> / <?= (int) $pages ?>
                 · <?= (int) $pageSize ?> per page
                 · <?= (int) $pageSize ?> per page
@@ -44,11 +44,11 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
 
 
     <!-- Filter form -->
     <!-- Filter form -->
     <form method="get" action="/audit"
     <form method="get" action="/audit"
-          class="rounded-lg border bg-white p-4 grid grid-cols-1 md:grid-cols-6 gap-3">
+          class="rounded-lg border bg-white p-4 grid grid-cols-1 md:grid-cols-6 gap-3 dark:bg-slate-800 dark:border-slate-700">
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">User</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">User</span>
             <select name="user_email"
             <select name="user_email"
-                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                 <option value="">Any</option>
                 <option value="">Any</option>
                 <?php foreach ($users as $u): ?>
                 <?php foreach ($users as $u): ?>
                     <option value="<?= e($u) ?>" <?= $filters['user_email'] === $u ? 'selected' : '' ?>><?= e($u) ?></option>
                     <option value="<?= e($u) ?>" <?= $filters['user_email'] === $u ? 'selected' : '' ?>><?= e($u) ?></option>
@@ -57,9 +57,9 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
         </label>
         </label>
 
 
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">Action</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">Action</span>
             <select name="action"
             <select name="action"
-                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                 <option value="">Any</option>
                 <option value="">Any</option>
                 <?php foreach ($actions as $a): ?>
                 <?php foreach ($actions as $a): ?>
                     <option value="<?= e($a) ?>" <?= $filters['action'] === $a ? 'selected' : '' ?>><?= e($a) ?></option>
                     <option value="<?= e($a) ?>" <?= $filters['action'] === $a ? 'selected' : '' ?>><?= e($a) ?></option>
@@ -68,9 +68,9 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
         </label>
         </label>
 
 
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">Entity type</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">Entity type</span>
             <select name="entity_type"
             <select name="entity_type"
-                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                    class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                 <option value="">Any</option>
                 <option value="">Any</option>
                 <?php foreach ($entityTypes as $t): ?>
                 <?php foreach ($entityTypes as $t): ?>
                     <option value="<?= e($t) ?>" <?= $filters['entity_type'] === $t ? 'selected' : '' ?>><?= e($t) ?></option>
                     <option value="<?= e($t) ?>" <?= $filters['entity_type'] === $t ? 'selected' : '' ?>><?= e($t) ?></option>
@@ -79,44 +79,44 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
         </label>
         </label>
 
 
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">Entity ID (contains)</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">Entity ID (contains)</span>
             <input name="entity_id" type="text"
             <input name="entity_id" type="text"
                    value="<?= e($filters['entity_id']) ?>"
                    value="<?= e($filters['entity_id']) ?>"
-                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
+                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
         </label>
         </label>
 
 
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">From date</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">From date</span>
             <input name="from_date" type="date"
             <input name="from_date" type="date"
                    value="<?= e($filters['from_date']) ?>"
                    value="<?= e($filters['from_date']) ?>"
-                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
+                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
         </label>
         </label>
 
 
         <label class="block">
         <label class="block">
-            <span class="text-xs text-slate-600">To date</span>
+            <span class="text-xs text-slate-600 dark:text-slate-400">To date</span>
             <input name="to_date" type="date"
             <input name="to_date" type="date"
                    value="<?= e($filters['to_date']) ?>"
                    value="<?= e($filters['to_date']) ?>"
-                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
+                   class="mt-1 block w-full rounded border border-slate-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
         </label>
         </label>
 
 
         <div class="md:col-span-6 flex items-center justify-end gap-2">
         <div class="md:col-span-6 flex items-center justify-end gap-2">
             <?php if ($anyFilter): ?>
             <?php if ($anyFilter): ?>
-                <a href="/audit" class="text-sm text-slate-600 hover:underline">Clear</a>
+                <a href="/audit" class="text-sm text-slate-600 hover:underline dark:text-slate-400 dark:hover:text-slate-200">Clear</a>
             <?php endif; ?>
             <?php endif; ?>
             <button type="submit"
             <button type="submit"
-                    class="rounded bg-slate-900 text-white px-4 py-1.5 text-sm font-medium hover:bg-slate-800">
+                    class="rounded bg-slate-900 text-white px-4 py-1.5 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                 Apply
                 Apply
             </button>
             </button>
         </div>
         </div>
     </form>
     </form>
 
 
     <!-- Rows -->
     <!-- Rows -->
-    <div class="rounded-lg border bg-white overflow-hidden">
+    <div class="rounded-lg border bg-white overflow-hidden dark:bg-slate-800 dark:border-slate-700">
         <?php if ($rows === []): ?>
         <?php if ($rows === []): ?>
-            <div class="p-8 text-center text-slate-500 text-sm">No audit rows match.</div>
+            <div class="p-8 text-center text-slate-500 text-sm dark:text-slate-400">No audit rows match.</div>
         <?php else: ?>
         <?php else: ?>
             <table class="min-w-full text-sm">
             <table class="min-w-full text-sm">
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="text-left px-3 py-2 font-semibold">When (UTC)</th>
                         <th class="text-left px-3 py-2 font-semibold">When (UTC)</th>
                         <th class="text-left px-3 py-2 font-semibold">User</th>
                         <th class="text-left px-3 py-2 font-semibold">User</th>
@@ -126,7 +126,7 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
                         <th class="text-left px-3 py-2 font-semibold">Origin</th>
                         <th class="text-left px-3 py-2 font-semibold">Origin</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100 align-top">
+                <tbody class="divide-y divide-slate-100 align-top dark:divide-slate-700">
                     <?php foreach ($rows as $r): ?>
                     <?php foreach ($rows as $r): ?>
                         <tr>
                         <tr>
                             <td class="px-3 py-2 font-mono text-xs whitespace-nowrap">
                             <td class="px-3 py-2 font-mono text-xs whitespace-nowrap">
@@ -135,21 +135,21 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
                             <td class="px-3 py-2">
                             <td class="px-3 py-2">
                                 <?= $r['user_email'] !== null && $r['user_email'] !== ''
                                 <?= $r['user_email'] !== null && $r['user_email'] !== ''
                                     ? e((string) $r['user_email'])
                                     ? e((string) $r['user_email'])
-                                    : '<span class="text-slate-400">—</span>' ?>
+                                    : '<span class="text-slate-400 dark:text-slate-500">—</span>' ?>
                             </td>
                             </td>
                             <td class="px-3 py-2">
                             <td class="px-3 py-2">
                                 <span class="inline-block px-1.5 py-0.5 rounded text-xs font-mono
                                 <span class="inline-block px-1.5 py-0.5 rounded text-xs font-mono
                                     <?php
                                     <?php
                                     $action = (string) $r['action'];
                                     $action = (string) $r['action'];
                                     echo match ($action) {
                                     echo match ($action) {
-                                        'CREATE'          => 'bg-green-100 text-green-800',
-                                        'UPDATE'          => 'bg-blue-100 text-blue-800',
-                                        'DELETE'          => 'bg-red-100 text-red-800',
-                                        'LOGIN'           => 'bg-slate-100 text-slate-700',
-                                        'LOGOUT'          => 'bg-slate-100 text-slate-700',
-                                        'LOGIN_FAILED'    => 'bg-amber-100 text-amber-800',
-                                        'BOOTSTRAP_ADMIN' => 'bg-purple-100 text-purple-800',
-                                        default           => 'bg-slate-100 text-slate-700',
+                                        'CREATE'          => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
+                                        'UPDATE'          => 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
+                                        'DELETE'          => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
+                                        'LOGIN'           => 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
+                                        'LOGOUT'          => 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
+                                        'LOGIN_FAILED'    => 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
+                                        'BOOTSTRAP_ADMIN' => 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
+                                        default           => 'bg-slate-100 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
                                     };
                                     };
                                     ?>">
                                     ?>">
                                     <?= e($action) ?>
                                     <?= e($action) ?>
@@ -158,35 +158,35 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
                             <td class="px-3 py-2 font-mono text-xs whitespace-nowrap">
                             <td class="px-3 py-2 font-mono text-xs whitespace-nowrap">
                                 <?= e((string) $r['entity_type']) ?>
                                 <?= e((string) $r['entity_type']) ?>
                                 <?php if ($r['entity_id'] !== null): ?>
                                 <?php if ($r['entity_id'] !== null): ?>
-                                    <span class="text-slate-500">/</span>
+                                    <span class="text-slate-500 dark:text-slate-400">/</span>
                                     <?= e((string) $r['entity_id']) ?>
                                     <?= e((string) $r['entity_id']) ?>
                                 <?php endif; ?>
                                 <?php endif; ?>
                             </td>
                             </td>
                             <td class="px-3 py-2">
                             <td class="px-3 py-2">
                                 <?php $b = $prettyJson($r['before_json'] ?? null); $a = $prettyJson($r['after_json'] ?? null); ?>
                                 <?php $b = $prettyJson($r['before_json'] ?? null); $a = $prettyJson($r['after_json'] ?? null); ?>
                                 <?php if ($b === '' && $a === ''): ?>
                                 <?php if ($b === '' && $a === ''): ?>
-                                    <span class="text-slate-400 text-xs">—</span>
+                                    <span class="text-slate-400 text-xs dark:text-slate-500">—</span>
                                 <?php else: ?>
                                 <?php else: ?>
                                     <details class="text-xs">
                                     <details class="text-xs">
-                                        <summary class="cursor-pointer text-slate-600 hover:text-slate-900">
+                                        <summary class="cursor-pointer text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-slate-100">
                                             <?= $b !== '' && $a !== '' ? 'before / after'
                                             <?= $b !== '' && $a !== '' ? 'before / after'
                                                 : ($b !== '' ? 'before only' : 'after only') ?>
                                                 : ($b !== '' ? 'before only' : 'after only') ?>
                                         </summary>
                                         </summary>
                                         <?php if ($b !== ''): ?>
                                         <?php if ($b !== ''): ?>
-                                            <div class="mt-1 text-[11px] text-slate-500">before</div>
-                                            <pre class="mt-1 p-2 bg-slate-50 rounded overflow-x-auto"><?= e($b) ?></pre>
+                                            <div class="mt-1 text-[11px] text-slate-500 dark:text-slate-400">before</div>
+                                            <pre class="mt-1 p-2 bg-slate-50 rounded overflow-x-auto dark:bg-slate-900 dark:text-slate-200"><?= e($b) ?></pre>
                                         <?php endif; ?>
                                         <?php endif; ?>
                                         <?php if ($a !== ''): ?>
                                         <?php if ($a !== ''): ?>
-                                            <div class="mt-1 text-[11px] text-slate-500">after</div>
-                                            <pre class="mt-1 p-2 bg-slate-50 rounded overflow-x-auto"><?= e($a) ?></pre>
+                                            <div class="mt-1 text-[11px] text-slate-500 dark:text-slate-400">after</div>
+                                            <pre class="mt-1 p-2 bg-slate-50 rounded overflow-x-auto dark:bg-slate-900 dark:text-slate-200"><?= e($a) ?></pre>
                                         <?php endif; ?>
                                         <?php endif; ?>
                                     </details>
                                     </details>
                                 <?php endif; ?>
                                 <?php endif; ?>
                             </td>
                             </td>
-                            <td class="px-3 py-2 text-xs text-slate-500 whitespace-nowrap">
+                            <td class="px-3 py-2 text-xs text-slate-500 whitespace-nowrap dark:text-slate-400">
                                 <?= e((string) ($r['ip_address'] ?? '')) ?>
                                 <?= e((string) ($r['ip_address'] ?? '')) ?>
                                 <?php if (!empty($r['user_agent'])): ?>
                                 <?php if (!empty($r['user_agent'])): ?>
-                                    <span class="text-slate-300"
+                                    <span class="text-slate-300 dark:text-slate-500"
                                           title="<?= e((string) $r['user_agent']) ?>">(UA)</span>
                                           title="<?= e((string) $r['user_agent']) ?>">(UA)</span>
                                 <?php endif; ?>
                                 <?php endif; ?>
                             </td>
                             </td>
@@ -205,12 +205,12 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
             $nextQs = $qsWithout($filters, 'page', ['page' => min($pages, $page + 1)]);
             $nextQs = $qsWithout($filters, 'page', ['page' => min($pages, $page + 1)]);
             ?>
             ?>
             <a href="/audit<?= e($prevQs) ?>"
             <a href="/audit<?= e($prevQs) ?>"
-               class="<?= $page <= 1 ? 'pointer-events-none text-slate-300' : 'text-blue-700 hover:underline' ?>">
+               class="<?= $page <= 1 ? 'pointer-events-none text-slate-300 dark:text-slate-600' : 'text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300' ?>">
                 ← Previous
                 ← Previous
             </a>
             </a>
-            <span class="text-slate-600">Page <?= (int) $page ?> of <?= (int) $pages ?></span>
+            <span class="text-slate-600 dark:text-slate-400">Page <?= (int) $page ?> of <?= (int) $pages ?></span>
             <a href="/audit<?= e($nextQs) ?>"
             <a href="/audit<?= e($nextQs) ?>"
-               class="<?= $page >= $pages ? 'pointer-events-none text-slate-300' : 'text-blue-700 hover:underline' ?>">
+               class="<?= $page >= $pages ? 'pointer-events-none text-slate-300 dark:text-slate-600' : 'text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300' ?>">
                 Next →
                 Next →
             </a>
             </a>
         </nav>
         </nav>

+ 9 - 9
views/auth/local.php

@@ -5,15 +5,15 @@
 use function App\Http\e;
 use function App\Http\e;
 ?>
 ?>
 <section class="max-w-md mx-auto mt-6">
 <section class="max-w-md mx-auto mt-6">
-    <div class="rounded-lg border bg-white p-6">
+    <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
         <h1 class="text-xl font-semibold tracking-tight">Local admin sign-in</h1>
         <h1 class="text-xl font-semibold tracking-tight">Local admin sign-in</h1>
-        <p class="text-slate-600 text-sm mt-1">
+        <p class="text-slate-600 text-sm mt-1 dark:text-slate-400">
             Use this form only while Entra ID is not yet configured. Credentials
             Use this form only while Entra ID is not yet configured. Credentials
             come from the <code>LOCAL_ADMIN_*</code> environment variables.
             come from the <code>LOCAL_ADMIN_*</code> environment variables.
         </p>
         </p>
 
 
         <?php if ($error): ?>
         <?php if ($error): ?>
-            <div class="mt-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
+            <div class="mt-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:bg-red-900 dark:border-red-800 dark:text-red-200">
                 Email or password did not match.
                 Email or password did not match.
             </div>
             </div>
         <?php endif; ?>
         <?php endif; ?>
@@ -23,29 +23,29 @@ use function App\Http\e;
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
 
 
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Email</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Email</span>
                 <input type="email" name="email" required
                 <input type="email" name="email" required
                        value="<?= e($email) ?>"
                        value="<?= e($email) ?>"
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                               px-3 py-2 border focus:outline-none focus:ring-2
                               px-3 py-2 border focus:outline-none focus:ring-2
-                              focus:ring-slate-400">
+                              focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
 
 
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Password</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Password</span>
                 <input type="password" name="password" required autofocus
                 <input type="password" name="password" required autofocus
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                               px-3 py-2 border focus:outline-none focus:ring-2
                               px-3 py-2 border focus:outline-none focus:ring-2
-                              focus:ring-slate-400">
+                              focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
 
 
             <button type="submit"
             <button type="submit"
-                    class="w-full rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800">
+                    class="w-full rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                 Sign in
                 Sign in
             </button>
             </button>
         </form>
         </form>
 
 
-        <p class="text-xs text-slate-500 mt-4">
+        <p class="text-xs text-slate-500 mt-4 dark:text-slate-400">
             <a href="/" class="hover:underline">← Back</a>
             <a href="/" class="hover:underline">← Back</a>
         </p>
         </p>
     </div>
     </div>

+ 27 - 27
views/home.php

@@ -12,33 +12,33 @@ $sprintRows = $sprintRows ?? [];
 ?>
 ?>
 <section class="space-y-6">
 <section class="space-y-6">
     <?php if ($authError ?? false): ?>
     <?php if ($authError ?? false): ?>
-        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
+        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:bg-red-900 dark:border-red-800 dark:text-red-200">
             Sign-in failed. Check the server logs or the audit log for details.
             Sign-in failed. Check the server logs or the audit log for details.
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
 
 
     <?php if ($currentUser === null): ?>
     <?php if ($currentUser === null): ?>
-        <div class="rounded-lg border bg-white p-6">
+        <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
             <h1 class="text-2xl font-semibold tracking-tight">Sprint Planner</h1>
             <h1 class="text-2xl font-semibold tracking-tight">Sprint Planner</h1>
-            <p class="text-slate-600 mt-2 max-w-prose">
+            <p class="text-slate-600 mt-2 max-w-prose dark:text-slate-400">
                 Sign in with your Microsoft account to get started. The first person
                 Sign in with your Microsoft account to get started. The first person
                 to sign in becomes the admin automatically.
                 to sign in becomes the admin automatically.
             </p>
             </p>
             <div class="mt-4 flex flex-wrap items-center gap-3">
             <div class="mt-4 flex flex-wrap items-center gap-3">
                 <?php if ($oidcConfigured): ?>
                 <?php if ($oidcConfigured): ?>
                     <a href="/auth/login"
                     <a href="/auth/login"
-                       class="inline-flex items-center gap-2 rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800">
+                       class="inline-flex items-center gap-2 rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                         Sign in with Microsoft
                         Sign in with Microsoft
                     </a>
                     </a>
                 <?php endif; ?>
                 <?php endif; ?>
                 <?php if ($localAdminEnabled): ?>
                 <?php if ($localAdminEnabled): ?>
                     <a href="/auth/local"
                     <a href="/auth/local"
-                       class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-4 py-2 text-sm font-medium hover:bg-slate-100">
+                       class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-4 py-2 text-sm font-medium hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                         Sign in as local admin
                         Sign in as local admin
                     </a>
                     </a>
                 <?php endif; ?>
                 <?php endif; ?>
                 <?php if (!$oidcConfigured && !$localAdminEnabled): ?>
                 <?php if (!$oidcConfigured && !$localAdminEnabled): ?>
-                    <span class="inline-block rounded-md bg-slate-100 text-slate-600 px-3 py-2 text-sm">
+                    <span class="inline-block rounded-md bg-slate-100 text-slate-600 px-3 py-2 text-sm dark:bg-slate-700 dark:text-slate-300">
                         No sign-in method configured. Set <code>ENTRA_*</code> or
                         No sign-in method configured. Set <code>ENTRA_*</code> or
                         <code>LOCAL_ADMIN_*</code> in <code>.env</code>.
                         <code>LOCAL_ADMIN_*</code> in <code>.env</code>.
                     </span>
                     </span>
@@ -49,29 +49,29 @@ $sprintRows = $sprintRows ?? [];
         <div class="flex items-end justify-between gap-4">
         <div class="flex items-end justify-between gap-4">
             <div>
             <div>
                 <h1 class="text-2xl font-semibold tracking-tight">Sprints</h1>
                 <h1 class="text-2xl font-semibold tracking-tight">Sprints</h1>
-                <p class="text-slate-600 mt-1 text-sm">
+                <p class="text-slate-600 mt-1 text-sm dark:text-slate-400">
                     <?= count($sprintRows) ?> sprint<?= count($sprintRows) === 1 ? '' : 's' ?>.
                     <?= count($sprintRows) ?> sprint<?= count($sprintRows) === 1 ? '' : 's' ?>.
                 </p>
                 </p>
             </div>
             </div>
             <?php if ($currentUser->isAdmin): ?>
             <?php if ($currentUser->isAdmin): ?>
                 <a href="/sprints/new"
                 <a href="/sprints/new"
-                   class="inline-flex items-center gap-2 rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800">
+                   class="inline-flex items-center gap-2 rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                     New sprint
                     New sprint
                 </a>
                 </a>
             <?php endif; ?>
             <?php endif; ?>
         </div>
         </div>
 
 
-        <div class="rounded-lg border bg-white overflow-hidden">
+        <div class="rounded-lg border bg-white overflow-hidden dark:bg-slate-800 dark:border-slate-700">
             <?php if ($sprintRows === []): ?>
             <?php if ($sprintRows === []): ?>
-                <div class="p-8 text-center text-slate-500 text-sm">
+                <div class="p-8 text-center text-slate-500 text-sm dark:text-slate-400">
                     No sprints yet.
                     No sprints yet.
                     <?php if ($currentUser->isAdmin): ?>
                     <?php if ($currentUser->isAdmin): ?>
-                        <a href="/sprints/new" class="text-blue-700 hover:underline">Create the first one</a>.
+                        <a href="/sprints/new" class="text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300">Create the first one</a>.
                     <?php endif; ?>
                     <?php endif; ?>
                 </div>
                 </div>
             <?php else: ?>
             <?php else: ?>
                 <table class="min-w-full text-sm">
                 <table class="min-w-full text-sm">
-                    <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                    <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                         <tr>
                         <tr>
                             <th class="text-left px-4 py-2 font-semibold">Name</th>
                             <th class="text-left px-4 py-2 font-semibold">Name</th>
                             <th class="text-left px-4 py-2 font-semibold">Dates</th>
                             <th class="text-left px-4 py-2 font-semibold">Dates</th>
@@ -81,16 +81,16 @@ $sprintRows = $sprintRows ?? [];
                             <th class="text-left px-4 py-2 font-semibold">Status</th>
                             <th class="text-left px-4 py-2 font-semibold">Status</th>
                         </tr>
                         </tr>
                     </thead>
                     </thead>
-                    <tbody class="divide-y divide-slate-100">
+                    <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                         <?php foreach ($sprintRows as $row): $s = $row['sprint']; ?>
                         <?php foreach ($sprintRows as $row): $s = $row['sprint']; ?>
-                            <tr class="hover:bg-slate-50 cursor-pointer"
+                            <tr class="hover:bg-slate-50 cursor-pointer dark:hover:bg-slate-700"
                                 data-href="/sprints/<?= (int) $s->id ?>">
                                 data-href="/sprints/<?= (int) $s->id ?>">
                                 <td class="px-4 py-2 font-medium">
                                 <td class="px-4 py-2 font-medium">
                                     <a href="/sprints/<?= (int) $s->id ?>" class="hover:underline">
                                     <a href="/sprints/<?= (int) $s->id ?>" class="hover:underline">
                                         <?= e($s->name) ?>
                                         <?= e($s->name) ?>
                                     </a>
                                     </a>
                                 </td>
                                 </td>
-                                <td class="px-4 py-2 text-slate-600">
+                                <td class="px-4 py-2 text-slate-600 dark:text-slate-400">
                                     <?= e($s->startDate) ?> – <?= e($s->endDate) ?>
                                     <?= e($s->startDate) ?> – <?= e($s->endDate) ?>
                                 </td>
                                 </td>
                                 <td class="px-4 py-2 text-right font-mono"><?= (int) $row['nWorkers'] ?></td>
                                 <td class="px-4 py-2 text-right font-mono"><?= (int) $row['nWorkers'] ?></td>
@@ -100,9 +100,9 @@ $sprintRows = $sprintRows ?? [];
                                 </td>
                                 </td>
                                 <td class="px-4 py-2">
                                 <td class="px-4 py-2">
                                     <?php if ($s->isArchived): ?>
                                     <?php if ($s->isArchived): ?>
-                                        <span class="inline-block px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded">archived</span>
+                                        <span class="inline-block px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded dark:bg-slate-700 dark:text-slate-300">archived</span>
                                     <?php else: ?>
                                     <?php else: ?>
-                                        <span class="inline-block px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded">active</span>
+                                        <span class="inline-block px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded dark:bg-green-900 dark:text-green-200">active</span>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                             </tr>
                             </tr>
@@ -114,29 +114,29 @@ $sprintRows = $sprintRows ?? [];
     <?php endif; ?>
     <?php endif; ?>
 
 
     <?php if ($currentUser === null || $currentUser->isAdmin): ?>
     <?php if ($currentUser === null || $currentUser->isAdmin): ?>
-    <details class="rounded-lg border bg-white p-4">
-        <summary class="text-sm font-semibold text-slate-700 uppercase tracking-wider cursor-pointer">Runtime</summary>
+    <details class="rounded-lg border bg-white p-4 dark:bg-slate-800 dark:border-slate-700">
+        <summary class="text-sm font-semibold text-slate-700 uppercase tracking-wider cursor-pointer dark:text-slate-200">Runtime</summary>
         <dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-6 gap-y-1 text-sm">
         <dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-6 gap-y-1 text-sm">
-            <dt class="text-slate-500">PHP</dt>
+            <dt class="text-slate-500 dark:text-slate-400">PHP</dt>
             <dd class="font-mono"><?= e(PHP_VERSION) ?></dd>
             <dd class="font-mono"><?= e(PHP_VERSION) ?></dd>
 
 
-            <dt class="text-slate-500">APP_ENV</dt>
+            <dt class="text-slate-500 dark:text-slate-400">APP_ENV</dt>
             <dd class="font-mono"><?= e($appEnv) ?></dd>
             <dd class="font-mono"><?= e($appEnv) ?></dd>
 
 
-            <dt class="text-slate-500">SQLite file</dt>
+            <dt class="text-slate-500 dark:text-slate-400">SQLite file</dt>
             <dd class="font-mono break-all"><?= e($dbPath) ?></dd>
             <dd class="font-mono break-all"><?= e($dbPath) ?></dd>
 
 
-            <dt class="text-slate-500">Schema version</dt>
+            <dt class="text-slate-500 dark:text-slate-400">Schema version</dt>
             <dd class="font-mono"><?= e($schemaVersion) ?></dd>
             <dd class="font-mono"><?= e($schemaVersion) ?></dd>
 
 
-            <dt class="text-slate-500">OIDC</dt>
+            <dt class="text-slate-500 dark:text-slate-400">OIDC</dt>
             <dd class="font-mono"><?= $oidcConfigured ? 'configured' : 'not configured' ?></dd>
             <dd class="font-mono"><?= $oidcConfigured ? 'configured' : 'not configured' ?></dd>
 
 
-            <dt class="text-slate-500">Local admin</dt>
+            <dt class="text-slate-500 dark:text-slate-400">Local admin</dt>
             <dd class="font-mono"><?= $localAdminEnabled ? 'enabled' : 'disabled' ?></dd>
             <dd class="font-mono"><?= $localAdminEnabled ? 'enabled' : 'disabled' ?></dd>
         </dl>
         </dl>
-        <p class="mt-4 text-xs text-slate-500">
-            Liveness probe: <a class="text-blue-700 hover:underline" href="/healthz"><code>/healthz</code></a>
+        <p class="mt-4 text-xs text-slate-500 dark:text-slate-400">
+            Liveness probe: <a class="text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300" href="/healthz"><code>/healthz</code></a>
         </p>
         </p>
     </details>
     </details>
     <?php endif; ?>
     <?php endif; ?>

+ 21 - 15
views/layout.php

@@ -13,6 +13,7 @@ $csrfToken   = $csrfToken   ?? '';
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <title><?= e($title ?? 'Sprint Planner') ?></title>
     <title><?= e($title ?? 'Sprint Planner') ?></title>
+    <script src="/assets/js/theme-init.js"></script>
     <link rel="stylesheet" href="/assets/css/app.css">
     <link rel="stylesheet" href="/assets/css/app.css">
     <link rel="stylesheet"
     <link rel="stylesheet"
           href="https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css">
           href="https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css">
@@ -20,22 +21,22 @@ $csrfToken   = $csrfToken   ?? '';
     <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"></script>
     <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"></script>
     <script src="/assets/js/app.js" defer></script>
     <script src="/assets/js/app.js" defer></script>
 </head>
 </head>
-<body class="bg-slate-50 text-slate-900 antialiased">
-    <header class="border-b bg-white">
+<body class="bg-slate-100 text-slate-900 antialiased dark:bg-slate-900 dark:text-slate-100">
+    <header class="border-b bg-white dark:bg-slate-800 dark:border-slate-700">
         <div class="max-w-7xl mx-auto px-4 py-3 flex items-center gap-4">
         <div class="max-w-7xl mx-auto px-4 py-3 flex items-center gap-4">
             <a href="/" class="font-semibold tracking-tight">Sprint Planner</a>
             <a href="/" class="font-semibold tracking-tight">Sprint Planner</a>
 
 
             <nav class="ml-auto flex items-center gap-4 text-sm">
             <nav class="ml-auto flex items-center gap-4 text-sm">
                 <?php if ($currentUser !== null): ?>
                 <?php if ($currentUser !== null): ?>
-                    <a href="/" class="text-slate-600 hover:text-slate-900 hover:underline">Sprints</a>
+                    <a href="/" class="text-slate-600 hover:text-slate-900 hover:underline dark:text-slate-300 dark:hover:text-slate-100">Sprints</a>
                     <?php if ($currentUser->isAdmin): ?>
                     <?php if ($currentUser->isAdmin): ?>
-                        <a href="/sprints/new" class="text-slate-600 hover:text-slate-900 hover:underline">New sprint</a>
+                        <a href="/sprints/new" class="text-slate-600 hover:text-slate-900 hover:underline dark:text-slate-300 dark:hover:text-slate-100">New sprint</a>
                     <?php endif; ?>
                     <?php endif; ?>
-                    <span class="text-slate-400">·</span>
-                    <span class="text-slate-600">
+                    <span class="text-slate-400 dark:text-slate-600">·</span>
+                    <span class="text-slate-600 dark:text-slate-300">
                         <?= e($currentUser->displayName) ?>
                         <?= e($currentUser->displayName) ?>
                         <?php if ($currentUser->isAdmin): ?>
                         <?php if ($currentUser->isAdmin): ?>
-                            <span class="ml-1 inline-block px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-amber-100 text-amber-800 rounded">admin</span>
+                            <span class="ml-1 inline-block px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-amber-100 text-amber-800 rounded dark:bg-amber-900 dark:text-amber-200">admin</span>
                         <?php endif; ?>
                         <?php endif; ?>
                     </span>
                     </span>
                     <div class="relative">
                     <div class="relative">
@@ -45,7 +46,7 @@ $csrfToken   = $csrfToken   ?? '';
                                 aria-haspopup="true"
                                 aria-haspopup="true"
                                 aria-controls="app-menu"
                                 aria-controls="app-menu"
                                 aria-label="Open menu"
                                 aria-label="Open menu"
-                                class="p-2 rounded-md hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                class="p-2 rounded-md hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">
                             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" class="block stroke-current" fill="none" stroke-width="2" stroke-linecap="round">
                             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" class="block stroke-current" fill="none" stroke-width="2" stroke-linecap="round">
                                 <line x1="3" y1="5"  x2="17" y2="5"></line>
                                 <line x1="3" y1="5"  x2="17" y2="5"></line>
                                 <line x1="3" y1="10" x2="17" y2="10"></line>
                                 <line x1="3" y1="10" x2="17" y2="10"></line>
@@ -56,20 +57,25 @@ $csrfToken   = $csrfToken   ?? '';
                              data-menu
                              data-menu
                              role="menu"
                              role="menu"
                              hidden
                              hidden
-                             class="absolute right-0 mt-2 min-w-[12rem] rounded-md border border-slate-200 bg-white shadow-lg py-1 z-10">
+                             class="absolute right-0 mt-2 min-w-[12rem] rounded-md border border-slate-200 bg-white shadow-lg py-1 z-10 dark:bg-slate-800 dark:border-slate-700">
                             <?php if ($currentUser->isAdmin): ?>
                             <?php if ($currentUser->isAdmin): ?>
                                 <a href="/workers" role="menuitem"
                                 <a href="/workers" role="menuitem"
-                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">Workers</a>
+                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">Workers</a>
                                 <a href="/users" role="menuitem"
                                 <a href="/users" role="menuitem"
-                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">Users</a>
+                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">Users</a>
                                 <a href="/audit" role="menuitem"
                                 <a href="/audit" role="menuitem"
-                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">Audit log</a>
-                                <hr class="my-1 border-slate-200">
+                                   class="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">Audit log</a>
                             <?php endif; ?>
                             <?php endif; ?>
+                            <button type="button" role="menuitem" data-theme-toggle
+                                    class="w-full text-left px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">
+                                <span>Theme</span>
+                                <span data-theme-label class="text-slate-500 dark:text-slate-400">Light</span>
+                            </button>
+                            <hr class="my-1 border-slate-200 dark:border-slate-700">
                             <form method="post" action="/auth/logout">
                             <form method="post" action="/auth/logout">
                                 <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
                                 <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
                                 <button type="submit" role="menuitem"
                                 <button type="submit" role="menuitem"
-                                        class="block w-full text-left px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 font-[inherit] focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                        class="block w-full text-left px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 font-[inherit] focus:outline-none focus:ring-2 focus:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-700">
                                     Sign out
                                     Sign out
                                 </button>
                                 </button>
                             </form>
                             </form>
@@ -77,7 +83,7 @@ $csrfToken   = $csrfToken   ?? '';
                     </div>
                     </div>
                 <?php else: ?>
                 <?php else: ?>
                     <a href="/auth/login"
                     <a href="/auth/login"
-                       class="text-blue-700 hover:underline">Sign in</a>
+                       class="text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300">Sign in</a>
                 <?php endif; ?>
                 <?php endif; ?>
             </nav>
             </nav>
         </div>
         </div>

+ 17 - 17
views/sprints/new.php

@@ -17,66 +17,66 @@ $errorMessages = [
 ?>
 ?>
 <section class="max-w-xl">
 <section class="max-w-xl">
     <h1 class="text-2xl font-semibold tracking-tight">New sprint</h1>
     <h1 class="text-2xl font-semibold tracking-tight">New sprint</h1>
-    <p class="text-slate-600 mt-1 text-sm">
+    <p class="text-slate-600 mt-1 text-sm dark:text-slate-400">
         Worker membership, weekly availability and tasks are configured on the
         Worker membership, weekly availability and tasks are configured on the
         sprint page after creation.
         sprint page after creation.
     </p>
     </p>
 
 
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
-        <div class="mt-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
+        <div class="mt-4 rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:bg-red-900 dark:border-red-800 dark:text-red-200">
             <?= e($errorMessages[$error]) ?>
             <?= e($errorMessages[$error]) ?>
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
 
 
-    <form method="post" action="/sprints" class="mt-6 space-y-4 rounded-lg border bg-white p-5">
+    <form method="post" action="/sprints" class="mt-6 space-y-4 rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
         <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
         <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
 
 
         <label class="block">
         <label class="block">
-            <span class="text-sm text-slate-700">Name</span>
+            <span class="text-sm text-slate-700 dark:text-slate-300">Name</span>
             <input name="name" type="text" required
             <input name="name" type="text" required
                    value="<?= e($form['name']) ?>"
                    value="<?= e($form['name']) ?>"
                    placeholder="e.g. Sprint 12"
                    placeholder="e.g. Sprint 12"
-                   class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                   class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
         </label>
         </label>
 
 
         <div class="grid grid-cols-2 gap-3">
         <div class="grid grid-cols-2 gap-3">
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Start date</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Start date</span>
                 <input name="start_date" type="date" required
                 <input name="start_date" type="date" required
                        value="<?= e($form['start_date']) ?>"
                        value="<?= e($form['start_date']) ?>"
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">End date</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">End date</span>
                 <input name="end_date" type="date" required
                 <input name="end_date" type="date" required
                        value="<?= e($form['end_date']) ?>"
                        value="<?= e($form['end_date']) ?>"
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
         </div>
         </div>
 
 
         <div class="grid grid-cols-2 gap-3">
         <div class="grid grid-cols-2 gap-3">
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Reserve (%)</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Reserve (%)</span>
                 <input name="reserve_fraction" type="number" min="0" max="100" step="1" required
                 <input name="reserve_fraction" type="number" min="0" max="100" step="1" required
                        value="<?= e($form['reserve_fraction']) ?>"
                        value="<?= e($form['reserve_fraction']) ?>"
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
-                <span class="text-xs text-slate-500">Reduction from raw capacity. The Excel uses 20%.</span>
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
+                <span class="text-xs text-slate-500 dark:text-slate-400">Reduction from raw capacity. The Excel uses 20%.</span>
             </label>
             </label>
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Weeks</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Weeks</span>
                 <input name="n_weeks" type="number" min="1" max="26" step="1" required
                 <input name="n_weeks" type="number" min="1" max="26" step="1" required
                        value="<?= e($form['n_weeks']) ?>"
                        value="<?= e($form['n_weeks']) ?>"
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
-                <span class="text-xs text-slate-500">Week rows get 5 days/week by default; edit on the sprint page.</span>
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
+                <span class="text-xs text-slate-500 dark:text-slate-400">Week rows get 5 days/week by default; edit on the sprint page.</span>
             </label>
             </label>
         </div>
         </div>
 
 
         <div class="flex gap-3 pt-2">
         <div class="flex gap-3 pt-2">
             <button type="submit"
             <button type="submit"
-                    class="rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800">
+                    class="rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                 Create sprint
                 Create sprint
             </button>
             </button>
-            <a href="/" class="inline-flex items-center rounded-md border border-slate-300 bg-white text-slate-700 px-4 py-2 text-sm hover:bg-slate-100">
+            <a href="/" class="inline-flex items-center rounded-md border border-slate-300 bg-white text-slate-700 px-4 py-2 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                 Cancel
                 Cancel
             </a>
             </a>
         </div>
         </div>

+ 48 - 47
views/sprints/present.php

@@ -42,6 +42,7 @@ if (!function_exists('fmt_days')) {
     <meta charset="utf-8">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <title><?= e($title ?? ($sprint->name . ' — present')) ?></title>
     <title><?= e($title ?? ($sprint->name . ' — present')) ?></title>
+    <script src="/assets/js/theme-init.js"></script>
     <link rel="stylesheet" href="/assets/css/app.css">
     <link rel="stylesheet" href="/assets/css/app.css">
     <link rel="stylesheet"
     <link rel="stylesheet"
           href="https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css">
           href="https://code.jquery.com/ui/1.13.3/themes/base/jquery-ui.css">
@@ -49,7 +50,7 @@ if (!function_exists('fmt_days')) {
     <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"></script>
     <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"></script>
     <script src="/assets/js/sprint-planner.js" defer></script>
     <script src="/assets/js/sprint-planner.js" defer></script>
 </head>
 </head>
-<body class="bg-white text-slate-900 antialiased">
+<body class="bg-white text-slate-900 antialiased dark:bg-slate-900 dark:text-slate-100">
 <main class="min-h-screen w-screen overflow-hidden beamer-root"
 <main class="min-h-screen w-screen overflow-hidden beamer-root"
       data-sprint-root
       data-sprint-root
       data-sprint-id="<?= (int) $sprint->id ?>"
       data-sprint-id="<?= (int) $sprint->id ?>"
@@ -57,29 +58,29 @@ if (!function_exists('fmt_days')) {
       data-reserve-fraction="<?= e(number_format($sprint->reserveFraction, 4, '.', '')) ?>"
       data-reserve-fraction="<?= e(number_format($sprint->reserveFraction, 4, '.', '')) ?>"
       data-beamer="1">
       data-beamer="1">
 
 
-    <header class="flex items-center justify-between gap-4 px-4 py-2 border-b bg-slate-50">
+    <header class="flex items-center justify-between gap-4 px-4 py-2 border-b bg-slate-50 dark:bg-slate-800 dark:border-slate-700">
         <div class="flex items-baseline gap-3">
         <div class="flex items-baseline gap-3">
             <h1 class="text-lg font-semibold tracking-tight"><?= e($sprint->name) ?></h1>
             <h1 class="text-lg font-semibold tracking-tight"><?= e($sprint->name) ?></h1>
-            <p class="text-slate-600 text-xs">
+            <p class="text-slate-600 text-xs dark:text-slate-400">
                 <?= e($sprint->startDate) ?> – <?= e($sprint->endDate) ?>
                 <?= e($sprint->startDate) ?> – <?= e($sprint->endDate) ?>
                 <?php if ($sprint->isArchived): ?>
                 <?php if ($sprint->isArchived): ?>
-                    · <span class="inline-block px-1.5 py-0.5 text-[10px] bg-slate-200 text-slate-600 rounded">archived</span>
+                    · <span class="inline-block px-1.5 py-0.5 text-[10px] bg-slate-200 text-slate-600 rounded dark:bg-slate-700 dark:text-slate-300">archived</span>
                 <?php endif; ?>
                 <?php endif; ?>
             </p>
             </p>
         </div>
         </div>
         <div class="flex items-center gap-3">
         <div class="flex items-center gap-3">
             <div data-status
             <div data-status
-                 class="text-xs border rounded px-2 py-0.5 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700">
+                 class="text-xs border rounded px-2 py-0.5 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300">
             </div>
             </div>
             <a href="/sprints/<?= (int) $sprint->id ?>"
             <a href="/sprints/<?= (int) $sprint->id ?>"
-               class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100">
+               class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                 Close
                 Close
             </a>
             </a>
         </div>
         </div>
     </header>
     </header>
 
 
     <?php if ($sprintWorkers === [] || $weeks === []): ?>
     <?php if ($sprintWorkers === [] || $weeks === []): ?>
-        <div class="m-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
+        <div class="m-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:bg-amber-900 dark:border-amber-800 dark:text-amber-200">
             <?php if ($weeks === []): ?>
             <?php if ($weeks === []): ?>
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
             <?php elseif ($sprintWorkers === []): ?>
             <?php elseif ($sprintWorkers === []): ?>
@@ -91,24 +92,24 @@ if (!function_exists('fmt_days')) {
     <!-- Task list — same structure as views/sprints/show.php's
     <!-- Task list — same structure as views/sprints/show.php's
          [data-task-section]. sprint-planner.js wires everything from the
          [data-task-section]. sprint-planner.js wires everything from the
          data-* attributes, so this is essentially a copy of that block. -->
          data-* attributes, so this is essentially a copy of that block. -->
-    <section class="rounded-lg border bg-white overflow-hidden m-2"
+    <section class="rounded-lg border bg-white overflow-hidden m-2 dark:bg-slate-800 dark:border-slate-700"
              data-task-section>
              data-task-section>
-        <div class="px-4 py-3 border-b bg-slate-50 flex flex-wrap items-center gap-2">
-            <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Tasks</h2>
+        <div class="px-4 py-3 border-b bg-slate-50 flex flex-wrap items-center gap-2 dark:bg-slate-700 dark:border-slate-700">
+            <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Tasks</h2>
 
 
             <!-- Toolbar -->
             <!-- Toolbar -->
             <div class="ml-auto flex flex-wrap items-center gap-2">
             <div class="ml-auto flex flex-wrap items-center gap-2">
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <button type="button" data-reset-filters
                 <button type="button" data-reset-filters
-                        class="hidden rounded border border-slate-300 px-2 py-1 text-sm bg-white text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                        class="hidden rounded border border-slate-300 px-2 py-1 text-sm bg-white text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                     Reset
                     Reset
                 </button>
                 </button>
 
 
                 <input type="search" data-task-search placeholder="Search…"
                 <input type="search" data-task-search placeholder="Search…"
-                       class="rounded border border-slate-300 px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="rounded border border-slate-300 px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
 
 
                 <select data-prio-filter
                 <select data-prio-filter
-                        class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                        class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                     <option value="">All prios</option>
                     <option value="">All prios</option>
                     <option value="1">Prio 1 only</option>
                     <option value="1">Prio 1 only</option>
                     <option value="2">Prio 2 only</option>
                     <option value="2">Prio 2 only</option>
@@ -117,26 +118,26 @@ if (!function_exists('fmt_days')) {
                 <!-- Multi-select owner filter -->
                 <!-- Multi-select owner filter -->
                 <div class="relative" data-owner-filter-root>
                 <div class="relative" data-owner-filter-root>
                     <button type="button" data-owner-filter-trigger
                     <button type="button" data-owner-filter-trigger
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
-                        Owners <span data-owner-filter-count class="text-slate-500"></span>
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
+                        Owners <span data-owner-filter-count class="text-slate-500 dark:text-slate-400"></span>
                     </button>
                     </button>
                     <div data-owner-filter-dropdown
                     <div data-owner-filter-dropdown
-                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg">
-                        <div class="px-3 py-2 text-xs text-slate-500 flex items-center justify-between">
+                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg dark:bg-slate-800 dark:border-slate-700">
+                        <div class="px-3 py-2 text-xs text-slate-500 flex items-center justify-between dark:text-slate-400">
                             <span>Owner</span>
                             <span>Owner</span>
                             <button type="button" data-owner-filter-clear
                             <button type="button" data-owner-filter-clear
-                                    class="text-blue-700 hover:underline">Clear</button>
+                                    class="text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300">Clear</button>
                         </div>
                         </div>
                         <div class="max-h-64 overflow-y-auto">
                         <div class="max-h-64 overflow-y-auto">
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
                                 <input type="checkbox" data-owner-filter-opt value="__none__"
                                 <input type="checkbox" data-owner-filter-opt value="__none__"
-                                       class="rounded border-slate-300">
-                                <span class="text-slate-500 italic">No owner</span>
+                                       class="rounded border-slate-300 dark:border-slate-600">
+                                <span class="text-slate-500 italic dark:text-slate-400">No owner</span>
                             </label>
                             </label>
                             <?php foreach ($ownerChoices as $ow): ?>
                             <?php foreach ($ownerChoices as $ow): ?>
-                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
+                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
                                     <input type="checkbox" data-owner-filter-opt value="<?= (int) $ow->id ?>"
                                     <input type="checkbox" data-owner-filter-opt value="<?= (int) $ow->id ?>"
-                                           class="rounded border-slate-300">
+                                           class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($ow->name) ?></span>
                                     <span><?= e($ow->name) ?></span>
                                 </label>
                                 </label>
                             <?php endforeach; ?>
                             <?php endforeach; ?>
@@ -146,9 +147,9 @@ if (!function_exists('fmt_days')) {
 
 
                 <!-- Focus filter -->
                 <!-- Focus filter -->
                 <div class="flex items-center gap-1" data-focus-filter-root>
                 <div class="flex items-center gap-1" data-focus-filter-root>
-                    <label for="data-focus-select" class="text-xs uppercase tracking-wider text-slate-500">Focus</label>
+                    <label for="data-focus-select" class="text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400">Focus</label>
                     <select id="data-focus-select" data-focus-select
                     <select id="data-focus-select" data-focus-select
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                         <option value="">All workers</option>
                         <option value="">All workers</option>
                         <?php foreach ($sprintWorkers as $sw): ?>
                         <?php foreach ($sprintWorkers as $sw): ?>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
@@ -159,28 +160,28 @@ if (!function_exists('fmt_days')) {
                 <!-- Column visibility -->
                 <!-- Column visibility -->
                 <div class="relative" data-columns-root>
                 <div class="relative" data-columns-root>
                     <button type="button" data-columns-trigger
                     <button type="button" data-columns-trigger
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                         Columns
                         Columns
                     </button>
                     </button>
                     <div data-columns-dropdown
                     <div data-columns-dropdown
-                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg">
-                        <div class="px-3 py-2 text-xs text-slate-500">Show columns</div>
+                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg dark:bg-slate-800 dark:border-slate-700">
+                        <div class="px-3 py-2 text-xs text-slate-500 dark:text-slate-400">Show columns</div>
                         <div class="max-h-64 overflow-y-auto">
                         <div class="max-h-64 overflow-y-auto">
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="owner" checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="owner" checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Owner</span>
                                 <span>Owner</span>
                             </label>
                             </label>
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="prio"  checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="prio"  checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Prio</span>
                                 <span>Prio</span>
                             </label>
                             </label>
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="tot"   checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="tot"   checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Tot</span>
                                 <span>Tot</span>
                             </label>
                             </label>
                             <?php foreach ($sprintWorkers as $sw): ?>
                             <?php foreach ($sprintWorkers as $sw): ?>
-                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                    <input type="checkbox" data-column-opt value="sw-<?= (int) $sw->id ?>" checked class="rounded border-slate-300">
+                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                    <input type="checkbox" data-column-opt value="sw-<?= (int) $sw->id ?>" checked class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($sw->workerName) ?></span>
                                     <span><?= e($sw->workerName) ?></span>
                                 </label>
                                 </label>
                             <?php endforeach; ?>
                             <?php endforeach; ?>
@@ -190,7 +191,7 @@ if (!function_exists('fmt_days')) {
 
 
                 <?php if ($currentUser->isAdmin): ?>
                 <?php if ($currentUser->isAdmin): ?>
                     <button type="button" data-add-task
                     <button type="button" data-add-task
-                            class="rounded bg-slate-900 text-white px-3 py-1 text-sm font-medium hover:bg-slate-800">
+                            class="rounded bg-slate-900 text-white px-3 py-1 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                         + Add task
                         + Add task
                     </button>
                     </button>
                 <?php endif; ?>
                 <?php endif; ?>
@@ -199,7 +200,7 @@ if (!function_exists('fmt_days')) {
 
 
         <div class="overflow-x-auto">
         <div class="overflow-x-auto">
             <table class="min-w-full text-sm" data-task-table>
             <table class="min-w-full text-sm" data-task-table>
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="w-6 px-2 py-2"></th>
                         <th class="w-6 px-2 py-2"></th>
                         <th class="text-left px-2 py-2 font-semibold cursor-pointer select-none"
                         <th class="text-left px-2 py-2 font-semibold cursor-pointer select-none"
@@ -220,10 +221,10 @@ if (!function_exists('fmt_days')) {
                         <th class="w-8 px-2 py-2"></th>
                         <th class="w-8 px-2 py-2"></th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100" data-task-tbody>
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-task-tbody>
                     <?php if ($tasks === []): ?>
                     <?php if ($tasks === []): ?>
                         <tr data-empty-tasks>
                         <tr data-empty-tasks>
-                            <td colspan="<?= 6 + count($sprintWorkers) ?>" class="px-3 py-8 text-center text-slate-500 text-sm">
+                            <td colspan="<?= 6 + count($sprintWorkers) ?>" class="px-3 py-8 text-center text-slate-500 text-sm dark:text-slate-400">
                                 No tasks yet.
                                 No tasks yet.
                                 <?php if ($currentUser->isAdmin): ?>
                                 <?php if ($currentUser->isAdmin): ?>
                                     Click <b>+ Add task</b> to start.
                                     Click <b>+ Add task</b> to start.
@@ -240,14 +241,14 @@ if (!function_exists('fmt_days')) {
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 <td class="px-2 py-1">
                                 <td class="px-2 py-1">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
-                                        <span class="handle cursor-grab text-slate-400 select-none">&#8801;</span>
+                                        <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                                 <td class="px-2 py-1 min-w-[14rem]">
                                 <td class="px-2 py-1 min-w-[14rem]">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <input type="text" data-title
                                         <input type="text" data-title
                                                value="<?= e($t->title) ?>"
                                                value="<?= e($t->title) ?>"
-                                               class="w-full rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                               class="w-full rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                     <?php else: ?>
                                     <?php else: ?>
                                         <span><?= e($t->title) ?></span>
                                         <span><?= e($t->title) ?></span>
                                     <?php endif; ?>
                                     <?php endif; ?>
@@ -255,7 +256,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1" data-col="owner">
                                 <td class="px-2 py-1" data-col="owner">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <select data-owner-select
                                         <select data-owner-select
-                                                class="w-full rounded border border-slate-200 px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                class="w-full rounded border border-slate-200 px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                             <option value="">—</option>
                                             <option value="">—</option>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                                 <option value="<?= (int) $ow->id ?>" <?= $t->ownerWorkerId === $ow->id ? 'selected' : '' ?>>
                                                 <option value="<?= (int) $ow->id ?>" <?= $t->ownerWorkerId === $ow->id ? 'selected' : '' ?>>
@@ -276,7 +277,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1 text-center" data-col="prio">
                                 <td class="px-2 py-1 text-center" data-col="prio">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <select data-prio-select
                                         <select data-prio-select
-                                                class="rounded border border-slate-200 px-2 py-1 bg-white font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                class="rounded border border-slate-200 px-2 py-1 bg-white font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                             <option value="1" <?= $t->priority === 1 ? 'selected' : '' ?>>1</option>
                                             <option value="1" <?= $t->priority === 1 ? 'selected' : '' ?>>1</option>
                                             <option value="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                             <option value="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                         </select>
                                         </select>
@@ -297,7 +298,7 @@ if (!function_exists('fmt_days')) {
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    data-assign
                                                    data-assign
                                                    data-sw-id="<?= (int) $sw->id ?>"
                                                    data-sw-id="<?= (int) $sw->id ?>"
-                                                   class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                   class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                         <?php else: ?>
                                         <?php else: ?>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                         <?php endif; ?>
                                         <?php endif; ?>
@@ -306,7 +307,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-1 py-1 text-right">
                                 <td class="px-1 py-1 text-right">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <button type="button" data-delete-task
                                         <button type="button" data-delete-task
-                                                class="text-sm text-red-600 hover:underline">×</button>
+                                                class="text-sm text-red-600 hover:underline dark:text-red-400">×</button>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                             </tr>
                             </tr>
@@ -315,7 +316,7 @@ if (!function_exists('fmt_days')) {
                 </tbody>
                 </tbody>
             </table>
             </table>
         </div>
         </div>
-        <div data-task-empty-filter class="hidden p-4 text-center text-slate-500 text-sm">
+        <div data-task-empty-filter class="hidden p-4 text-center text-slate-500 text-sm dark:text-slate-400">
             No tasks match the current filters.
             No tasks match the current filters.
         </div>
         </div>
     </section>
     </section>

+ 41 - 41
views/sprints/settings.php

@@ -15,52 +15,52 @@ use function App\Http\e;
 
 
     <header class="flex items-end justify-between gap-4">
     <header class="flex items-end justify-between gap-4">
         <div>
         <div>
-            <nav class="text-xs text-slate-500">
+            <nav class="text-xs text-slate-500 dark:text-slate-400">
                 <a href="/" class="hover:underline">Sprints</a> /
                 <a href="/" class="hover:underline">Sprints</a> /
                 <a href="/sprints/<?= (int) $sprint->id ?>" class="hover:underline"><?= e($sprint->name) ?></a> /
                 <a href="/sprints/<?= (int) $sprint->id ?>" class="hover:underline"><?= e($sprint->name) ?></a> /
             </nav>
             </nav>
             <h1 class="text-2xl font-semibold tracking-tight">Settings</h1>
             <h1 class="text-2xl font-semibold tracking-tight">Settings</h1>
         </div>
         </div>
         <div data-status
         <div data-status
-             class="text-sm border rounded px-3 py-1 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700">
+             class="text-sm border rounded px-3 py-1 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300">
         </div>
         </div>
     </header>
     </header>
 
 
     <!-- Sprint meta -->
     <!-- Sprint meta -->
-    <section class="rounded-lg border bg-white p-5 space-y-4">
-        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Sprint</h2>
+    <section class="rounded-lg border bg-white p-5 space-y-4 dark:bg-slate-800 dark:border-slate-700">
+        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Sprint</h2>
         <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
         <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
             <label class="md:col-span-2 block">
             <label class="md:col-span-2 block">
-                <span class="text-sm text-slate-700">Name</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Name</span>
                 <input data-meta name="name" type="text" value="<?= e($sprint->name) ?>"
                 <input data-meta name="name" type="text" value="<?= e($sprint->name) ?>"
-                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Start date</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Start date</span>
                 <input data-meta name="start_date" type="date" value="<?= e($sprint->startDate) ?>"
                 <input data-meta name="start_date" type="date" value="<?= e($sprint->startDate) ?>"
-                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">End date</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">End date</span>
                 <input data-meta name="end_date" type="date" value="<?= e($sprint->endDate) ?>"
                 <input data-meta name="end_date" type="date" value="<?= e($sprint->endDate) ?>"
-                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <label class="block">
             <label class="block">
-                <span class="text-sm text-slate-700">Reserve (%)</span>
+                <span class="text-sm text-slate-700 dark:text-slate-300">Reserve (%)</span>
                 <input data-meta name="reserve_fraction" type="number" min="0" max="100" step="1"
                 <input data-meta name="reserve_fraction" type="number" min="0" max="100" step="1"
                        value="<?= e(number_format($sprint->reserveFraction * 100, 0)) ?>"
                        value="<?= e(number_format($sprint->reserveFraction * 100, 0)) ?>"
-                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border border-slate-300 shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
         </div>
         </div>
-        <p class="text-xs text-slate-500">Changes save automatically.</p>
+        <p class="text-xs text-slate-500 dark:text-slate-400">Changes save automatically.</p>
     </section>
     </section>
 
 
     <!-- Weeks -->
     <!-- Weeks -->
-    <section class="rounded-lg border bg-white p-5 space-y-4">
+    <section class="rounded-lg border bg-white p-5 space-y-4 dark:bg-slate-800 dark:border-slate-700">
         <div class="flex items-end justify-between gap-4">
         <div class="flex items-end justify-between gap-4">
             <div>
             <div>
-                <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Weeks</h2>
-                <p class="text-xs text-slate-500 mt-1">
+                <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Weeks</h2>
+                <p class="text-xs text-slate-500 mt-1 dark:text-slate-400">
                     Current: <?= count($weeks) ?> week<?= count($weeks) === 1 ? '' : 's' ?>.
                     Current: <?= count($weeks) ?> week<?= count($weeks) === 1 ? '' : 's' ?>.
                     Tick the weekdays that are workdays for each week; the count feeds
                     Tick the weekdays that are workdays for each week; the count feeds
                     the Arbeitstage header on the sprint page.
                     the Arbeitstage header on the sprint page.
@@ -68,20 +68,20 @@ use function App\Http\e;
             </div>
             </div>
             <form data-weeks-form class="flex items-end gap-2">
             <form data-weeks-form class="flex items-end gap-2">
                 <label class="block">
                 <label class="block">
-                    <span class="text-xs text-slate-600">Set to</span>
+                    <span class="text-xs text-slate-600 dark:text-slate-400">Set to</span>
                     <input name="n_weeks" type="number" min="1" max="26" step="1"
                     <input name="n_weeks" type="number" min="1" max="26" step="1"
                            value="<?= count($weeks) ?>"
                            value="<?= count($weeks) ?>"
-                           class="mt-1 w-24 rounded-md border border-slate-300 shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                           class="mt-1 w-24 rounded-md border border-slate-300 shadow-sm px-3 py-2 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                 </label>
                 </label>
                 <button type="submit"
                 <button type="submit"
-                        class="rounded-md bg-slate-900 text-white px-3 py-2 text-sm font-medium hover:bg-slate-800">
+                        class="rounded-md bg-slate-900 text-white px-3 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                     Apply
                     Apply
                 </button>
                 </button>
             </form>
             </form>
         </div>
         </div>
-        <div class="overflow-hidden rounded border border-slate-200">
+        <div class="overflow-hidden rounded border border-slate-200 dark:border-slate-700">
             <table class="min-w-full text-sm" data-weeks-table>
             <table class="min-w-full text-sm" data-weeks-table>
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="text-left px-3 py-2 font-semibold">#</th>
                         <th class="text-left px-3 py-2 font-semibold">#</th>
                         <th class="text-left px-3 py-2 font-semibold">KW</th>
                         <th class="text-left px-3 py-2 font-semibold">KW</th>
@@ -92,7 +92,7 @@ use function App\Http\e;
                         <th class="text-right px-3 py-2 font-semibold">Arbeitstage</th>
                         <th class="text-right px-3 py-2 font-semibold">Arbeitstage</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <?php foreach ($weeks as $w): ?>
                     <?php foreach ($weeks as $w): ?>
                         <tr data-week-row data-week-id="<?= (int) $w->id ?>">
                         <tr data-week-row data-week-id="<?= (int) $w->id ?>">
                             <td class="px-3 py-2 font-mono"><?= (int) $w->sortOrder ?></td>
                             <td class="px-3 py-2 font-mono"><?= (int) $w->sortOrder ?></td>
@@ -106,7 +106,7 @@ use function App\Http\e;
                                            data-label="<?= e($label) ?>"
                                            data-label="<?= e($label) ?>"
                                            aria-label="<?= e($label) ?>"
                                            aria-label="<?= e($label) ?>"
                                            <?= $w->hasDay($label) ? 'checked' : '' ?>
                                            <?= $w->hasDay($label) ? 'checked' : '' ?>
-                                           class="rounded border-slate-300 focus:ring-slate-400">
+                                           class="rounded border-slate-300 focus:ring-slate-400 dark:border-slate-600 dark:focus:ring-slate-500">
                                 </td>
                                 </td>
                             <?php endforeach; ?>
                             <?php endforeach; ?>
                             <td class="px-3 py-2 font-mono text-right" data-week-count>
                             <td class="px-3 py-2 font-mono text-right" data-week-count>
@@ -117,61 +117,61 @@ use function App\Http\e;
                 </tbody>
                 </tbody>
             </table>
             </table>
         </div>
         </div>
-        <p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2">
+        <p class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2 dark:bg-amber-900 dark:border-amber-800 dark:text-amber-200">
             Reducing the week count deletes trailing weeks and any data attached to them.
             Reducing the week count deletes trailing weeks and any data attached to them.
         </p>
         </p>
     </section>
     </section>
 
 
     <!-- Worker picker -->
     <!-- Worker picker -->
-    <section class="rounded-lg border bg-white p-5 space-y-4">
-        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Workers</h2>
+    <section class="rounded-lg border bg-white p-5 space-y-4 dark:bg-slate-800 dark:border-slate-700">
+        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Workers</h2>
         <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
         <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
             <div>
             <div>
-                <h3 class="text-xs font-semibold text-slate-500 uppercase mb-2">Available</h3>
-                <div class="rounded border border-slate-200">
-                    <ul data-available class="divide-y divide-slate-100">
+                <h3 class="text-xs font-semibold text-slate-500 uppercase mb-2 dark:text-slate-400">Available</h3>
+                <div class="rounded border border-slate-200 dark:border-slate-700">
+                    <ul data-available class="divide-y divide-slate-100 dark:divide-slate-700">
                         <?php foreach ($availableWorkers as $w): ?>
                         <?php foreach ($availableWorkers as $w): ?>
-                            <li class="flex items-center gap-2 px-3 py-2 border-b last:border-b-0"
+                            <li class="flex items-center gap-2 px-3 py-2 border-b last:border-b-0 dark:border-slate-700"
                                 data-worker-id="<?= (int) $w->id ?>">
                                 data-worker-id="<?= (int) $w->id ?>">
                                 <span class="flex-1"><?= e($w->name) ?></span>
                                 <span class="flex-1"><?= e($w->name) ?></span>
-                                <button type="button" data-add class="text-sm text-blue-700 hover:underline">Add →</button>
+                                <button type="button" data-add class="text-sm text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300">Add →</button>
                             </li>
                             </li>
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                     </ul>
                     </ul>
                     <div data-empty-available
                     <div data-empty-available
-                         class="p-3 text-center text-xs text-slate-500"
+                         class="p-3 text-center text-xs text-slate-500 dark:text-slate-400"
                          <?= $availableWorkers === [] ? '' : 'style="display:none"' ?>>
                          <?= $availableWorkers === [] ? '' : 'style="display:none"' ?>>
                         No other active workers.
                         No other active workers.
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>
             <div>
             <div>
-                <h3 class="text-xs font-semibold text-slate-500 uppercase mb-2">In sprint (drag to reorder)</h3>
-                <div class="rounded border border-slate-200">
-                    <ul data-in-sprint class="divide-y divide-slate-100">
+                <h3 class="text-xs font-semibold text-slate-500 uppercase mb-2 dark:text-slate-400">In sprint (drag to reorder)</h3>
+                <div class="rounded border border-slate-200 dark:border-slate-700">
+                    <ul data-in-sprint class="divide-y divide-slate-100 dark:divide-slate-700">
                         <?php foreach ($sprintWorkers as $sw): ?>
                         <?php foreach ($sprintWorkers as $sw): ?>
-                            <li class="flex items-center gap-2 px-3 py-2 border-b bg-white last:border-b-0"
+                            <li class="flex items-center gap-2 px-3 py-2 border-b bg-white last:border-b-0 dark:bg-slate-800 dark:border-slate-700"
                                 data-sw-id="<?= (int) $sw->id ?>"
                                 data-sw-id="<?= (int) $sw->id ?>"
                                 data-worker-id="<?= (int) $sw->workerId ?>">
                                 data-worker-id="<?= (int) $sw->workerId ?>">
-                                <span class="handle cursor-grab text-slate-400 select-none">&#8801;</span>
+                                <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
                                 <span class="flex-1"><?= e($sw->workerName) ?></span>
                                 <span class="flex-1"><?= e($sw->workerName) ?></span>
                                 <input type="number" step="0.05" min="0" max="1"
                                 <input type="number" step="0.05" min="0" max="1"
                                        value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                        value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                        data-rtb
                                        data-rtb
-                                       class="w-20 rounded border border-slate-300 px-2 py-1 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
-                                <button type="button" data-remove class="text-sm text-red-600 hover:underline">Remove</button>
+                                       class="w-20 rounded border border-slate-300 px-2 py-1 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
+                                <button type="button" data-remove class="text-sm text-red-600 hover:underline dark:text-red-400">Remove</button>
                             </li>
                             </li>
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                     </ul>
                     </ul>
                     <div data-empty-sprint
                     <div data-empty-sprint
-                         class="p-3 text-center text-xs text-slate-500"
+                         class="p-3 text-center text-xs text-slate-500 dark:text-slate-400"
                          <?= $sprintWorkers === [] ? '' : 'style="display:none"' ?>>
                          <?= $sprintWorkers === [] ? '' : 'style="display:none"' ?>>
                         No workers assigned yet.
                         No workers assigned yet.
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>
         </div>
         </div>
-        <p class="text-xs text-slate-500">
+        <p class="text-xs text-slate-500 dark:text-slate-400">
             RTB (Run-the-Business) is informational and does not reduce computed capacity.
             RTB (Run-the-Business) is informational and does not reduce computed capacity.
         </p>
         </p>
     </section>
     </section>

+ 71 - 71
views/sprints/show.php

@@ -34,30 +34,30 @@ if (!function_exists('fmt_days')) {
 
 
     <header class="flex items-end justify-between gap-4">
     <header class="flex items-end justify-between gap-4">
         <div>
         <div>
-            <nav class="text-xs text-slate-500">
+            <nav class="text-xs text-slate-500 dark:text-slate-400">
                 <a href="/" class="hover:underline">Sprints</a> /
                 <a href="/" class="hover:underline">Sprints</a> /
             </nav>
             </nav>
             <h1 class="text-2xl font-semibold tracking-tight"><?= e($sprint->name) ?></h1>
             <h1 class="text-2xl font-semibold tracking-tight"><?= e($sprint->name) ?></h1>
-            <p class="text-slate-600 mt-1 text-sm">
+            <p class="text-slate-600 mt-1 text-sm dark:text-slate-400">
                 <?= e($sprint->startDate) ?> – <?= e($sprint->endDate) ?>
                 <?= e($sprint->startDate) ?> – <?= e($sprint->endDate) ?>
                 · Reserve <?= e(number_format($sprint->reserveFraction * 100, 0)) ?>%
                 · Reserve <?= e(number_format($sprint->reserveFraction * 100, 0)) ?>%
                 <?php if ($sprint->isArchived): ?>
                 <?php if ($sprint->isArchived): ?>
-                    · <span class="inline-block px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded">archived</span>
+                    · <span class="inline-block px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded dark:bg-slate-700 dark:text-slate-300">archived</span>
                 <?php endif; ?>
                 <?php endif; ?>
             </p>
             </p>
         </div>
         </div>
         <div class="flex items-center gap-3">
         <div class="flex items-center gap-3">
             <div data-status
             <div data-status
-                 class="text-sm border rounded px-3 py-1 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700">
+                 class="text-sm border rounded px-3 py-1 opacity-0 transition-opacity duration-200 border-slate-200 bg-slate-50 text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300">
             </div>
             </div>
             <a href="/sprints/<?= (int) $sprint->id ?>/present"
             <a href="/sprints/<?= (int) $sprint->id ?>/present"
                target="_blank" rel="noopener"
                target="_blank" rel="noopener"
-               class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100">
+               class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                 Present
                 Present
             </a>
             </a>
             <?php if ($currentUser->isAdmin): ?>
             <?php if ($currentUser->isAdmin): ?>
                 <a href="/sprints/<?= (int) $sprint->id ?>/settings"
                 <a href="/sprints/<?= (int) $sprint->id ?>/settings"
-                   class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100">
+                   class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                     Settings
                     Settings
                 </a>
                 </a>
             <?php endif; ?>
             <?php endif; ?>
@@ -65,7 +65,7 @@ if (!function_exists('fmt_days')) {
     </header>
     </header>
 
 
     <?php if ($sprintWorkers === [] || $weeks === []): ?>
     <?php if ($sprintWorkers === [] || $weeks === []): ?>
-        <div class="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
+        <div class="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:bg-amber-900 dark:border-amber-800 dark:text-amber-200">
             <?php if ($weeks === []): ?>
             <?php if ($weeks === []): ?>
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
             <?php elseif ($sprintWorkers === []): ?>
             <?php elseif ($sprintWorkers === []): ?>
@@ -75,29 +75,29 @@ if (!function_exists('fmt_days')) {
     <?php else: ?>
     <?php else: ?>
 
 
         <!-- Arbeitstage grid -->
         <!-- Arbeitstage grid -->
-        <section class="rounded-lg border bg-white overflow-x-auto">
+        <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
             <table class="min-w-full text-sm" data-arbeitstage>
             <table class="min-w-full text-sm" data-arbeitstage>
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
-                        <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10">&nbsp;</th>
+                        <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
                         <?php foreach ($weeks as $w): ?>
                         <?php foreach ($weeks as $w): ?>
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                                 <div class="font-mono">KW<?= (int) $w->isoWeek ?></div>
                                 <div class="font-mono">KW<?= (int) $w->isoWeek ?></div>
-                                <div class="text-[10px] text-slate-500 font-normal"><?= e($w->startDate) ?></div>
+                                <div class="text-[10px] text-slate-500 font-normal dark:text-slate-400"><?= e($w->startDate) ?></div>
                             </th>
                             </th>
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                         <th class="text-center px-2 py-2 font-semibold">Σ</th>
                         <th class="text-center px-2 py-2 font-semibold">Σ</th>
                         <th class="text-center px-2 py-2 font-semibold">RTB</th>
                         <th class="text-center px-2 py-2 font-semibold">RTB</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100" data-tbody>
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-tbody>
                     <!-- Arbeitstage row — derived from weekday selection in Sprint Settings. -->
                     <!-- Arbeitstage row — derived from weekday selection in Sprint Settings. -->
-                    <tr class="bg-slate-50">
-                        <th class="text-left px-3 py-2 font-semibold text-slate-700 sticky left-0 bg-slate-50">
+                    <tr class="bg-slate-50 dark:bg-slate-700">
+                        <th class="text-left px-3 py-2 font-semibold text-slate-700 sticky left-0 bg-slate-50 dark:bg-slate-700 dark:text-slate-200">
                             Arbeitstage
                             Arbeitstage
                             <?php if ($currentUser->isAdmin): ?>
                             <?php if ($currentUser->isAdmin): ?>
                                 <a href="/sprints/<?= (int) $sprint->id ?>/settings"
                                 <a href="/sprints/<?= (int) $sprint->id ?>/settings"
-                                   class="ml-1 text-[10px] font-normal text-slate-500 hover:underline"
+                                   class="ml-1 text-[10px] font-normal text-slate-500 hover:underline dark:text-slate-400"
                                    title="Pick weekdays in Settings">(edit)</a>
                                    title="Pick weekdays in Settings">(edit)</a>
                             <?php endif; ?>
                             <?php endif; ?>
                         </th>
                         </th>
@@ -108,7 +108,7 @@ if (!function_exists('fmt_days')) {
                                      title="<?= e(implode(' ', $w->activeDays())) ?: '—' ?>">
                                      title="<?= e(implode(' ', $w->activeDays())) ?: '—' ?>">
                                     <?php foreach (SprintWeek::DAY_LABELS as $bit => $_label): ?>
                                     <?php foreach (SprintWeek::DAY_LABELS as $bit => $_label): ?>
                                         <span class="inline-block h-2.5 w-2.5 rounded-full
                                         <span class="inline-block h-2.5 w-2.5 rounded-full
-                                            <?= $w->hasDay($_label) ? 'bg-green-500' : 'bg-slate-300' ?>"></span>
+                                            <?= $w->hasDay($_label) ? 'bg-green-500 dark:bg-green-400' : 'bg-slate-300 dark:bg-slate-600' ?>"></span>
                                     <?php endforeach; ?>
                                     <?php endforeach; ?>
                                 </div>
                                 </div>
                             </td>
                             </td>
@@ -123,10 +123,10 @@ if (!function_exists('fmt_days')) {
                     <?php foreach ($sprintWorkers as $sw): ?>
                     <?php foreach ($sprintWorkers as $sw): ?>
                         <?php $rowDays = $grid[$sw->id] ?? []; $rowSum = array_sum($rowDays); ?>
                         <?php $rowDays = $grid[$sw->id] ?? []; $rowSum = array_sum($rowDays); ?>
                         <tr data-sw-row data-sw-id="<?= (int) $sw->id ?>">
                         <tr data-sw-row data-sw-id="<?= (int) $sw->id ?>">
-                            <th class="text-left px-3 py-2 font-medium sticky left-0 bg-white z-10">
+                            <th class="text-left px-3 py-2 font-medium sticky left-0 bg-white z-10 dark:bg-slate-800">
                                 <span class="flex items-center gap-2">
                                 <span class="flex items-center gap-2">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
-                                        <span class="handle cursor-grab text-slate-400 select-none">&#8801;</span>
+                                        <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                     <?= e($sw->workerName) ?>
                                     <?= e($sw->workerName) ?>
                                 </span>
                                 </span>
@@ -138,7 +138,7 @@ if (!function_exists('fmt_days')) {
                                                value="<?= e(fmt_days($v)) ?>"
                                                value="<?= e(fmt_days($v)) ?>"
                                                data-day data-sw-id="<?= (int) $sw->id ?>"
                                                data-day data-sw-id="<?= (int) $sw->id ?>"
                                                data-week-id="<?= (int) $w->id ?>"
                                                data-week-id="<?= (int) $w->id ?>"
-                                               class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                               class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                     <?php else: ?>
                                     <?php else: ?>
                                         <span class="font-mono"><?= e(fmt_days($v)) ?></span>
                                         <span class="font-mono"><?= e(fmt_days($v)) ?></span>
                                     <?php endif; ?>
                                     <?php endif; ?>
@@ -153,7 +153,7 @@ if (!function_exists('fmt_days')) {
                                     <input type="number" min="0" max="1" step="0.05"
                                     <input type="number" min="0" max="1" step="0.05"
                                            value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                            value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                            data-rtb data-sw-id="<?= (int) $sw->id ?>"
                                            data-rtb data-sw-id="<?= (int) $sw->id ?>"
-                                           class="w-16 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                           class="w-16 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                 <?php else: ?>
                                 <?php else: ?>
                                     <span class="font-mono"><?= e(number_format($sw->rtb, 2, '.', '')) ?></span>
                                     <span class="font-mono"><?= e(number_format($sw->rtb, 2, '.', '')) ?></span>
                                 <?php endif; ?>
                                 <?php endif; ?>
@@ -165,14 +165,14 @@ if (!function_exists('fmt_days')) {
         </section>
         </section>
 
 
         <!-- Capacity summary — one column per worker, aligned with task columns in Phase 6 -->
         <!-- Capacity summary — one column per worker, aligned with task columns in Phase 6 -->
-        <section class="rounded-lg border bg-white overflow-x-auto">
-            <div class="px-4 py-2 border-b bg-slate-50 text-xs uppercase tracking-wider text-slate-600 font-semibold">
+        <section class="rounded-lg border bg-white overflow-x-auto dark:bg-slate-800 dark:border-slate-700">
+            <div class="px-4 py-2 border-b bg-slate-50 text-xs uppercase tracking-wider text-slate-600 font-semibold dark:bg-slate-700 dark:border-slate-700 dark:text-slate-300">
                 Capacity
                 Capacity
             </div>
             </div>
             <table class="min-w-full text-sm">
             <table class="min-w-full text-sm">
                 <thead>
                 <thead>
-                    <tr class="bg-slate-50 text-slate-600 text-xs">
-                        <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10">&nbsp;</th>
+                    <tr class="bg-slate-50 text-slate-600 text-xs dark:bg-slate-700 dark:text-slate-300">
+                        <th class="text-left px-3 py-2 font-semibold sticky left-0 bg-slate-50 z-10 dark:bg-slate-700">&nbsp;</th>
                         <?php foreach ($sprintWorkers as $sw): ?>
                         <?php foreach ($sprintWorkers as $sw): ?>
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                                 <?= e($sw->workerName) ?>
                                 <?= e($sw->workerName) ?>
@@ -180,9 +180,9 @@ if (!function_exists('fmt_days')) {
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <tr>
                     <tr>
-                        <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white">Ressourcen</th>
+                        <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">Ressourcen</th>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; ?>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; ?>
                             <td class="px-2 py-2 text-center font-mono"
                             <td class="px-2 py-2 text-center font-mono"
                                 data-cap-ressourcen data-sw-id="<?= (int) $sw->id ?>">
                                 data-cap-ressourcen data-sw-id="<?= (int) $sw->id ?>">
@@ -191,18 +191,18 @@ if (!function_exists('fmt_days')) {
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                     </tr>
                     </tr>
                     <tr>
                     <tr>
-                        <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white">− Reserven</th>
+                        <th class="text-left px-3 py-2 text-slate-700 font-medium sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">− Reserven</th>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; ?>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; ?>
-                            <td class="px-2 py-2 text-center font-mono text-slate-600"
+                            <td class="px-2 py-2 text-center font-mono text-slate-600 dark:text-slate-400"
                                 data-cap-after-reserves data-sw-id="<?= (int) $sw->id ?>">
                                 data-cap-after-reserves data-sw-id="<?= (int) $sw->id ?>">
                                 <?= e(fmt_days($c['after_reserves'] ?? 0.0)) ?>
                                 <?= e(fmt_days($c['after_reserves'] ?? 0.0)) ?>
                             </td>
                             </td>
                         <?php endforeach; ?>
                         <?php endforeach; ?>
                     </tr>
                     </tr>
                     <tr>
                     <tr>
-                        <th class="text-left px-3 py-2 text-slate-700 font-semibold sticky left-0 bg-white">Available</th>
+                        <th class="text-left px-3 py-2 text-slate-700 font-semibold sticky left-0 bg-white dark:bg-slate-800 dark:text-slate-200">Available</th>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; $av = (float) ($c['available'] ?? 0.0); ?>
                         <?php foreach ($sprintWorkers as $sw): $c = $capacity[$sw->id] ?? null; $av = (float) ($c['available'] ?? 0.0); ?>
-                            <td class="px-2 py-2 text-center font-mono font-semibold <?= $av < 0 ? 'text-red-700' : 'text-slate-900' ?>"
+                            <td class="px-2 py-2 text-center font-mono font-semibold <?= $av < 0 ? 'text-red-700 dark:text-red-400' : 'text-slate-900 dark:text-slate-100' ?>"
                                 data-cap-available data-sw-id="<?= (int) $sw->id ?>">
                                 data-cap-available data-sw-id="<?= (int) $sw->id ?>">
                                 <?= e(fmt_days($av)) ?>
                                 <?= e(fmt_days($av)) ?>
                             </td>
                             </td>
@@ -212,30 +212,30 @@ if (!function_exists('fmt_days')) {
             </table>
             </table>
         </section>
         </section>
 
 
-        <p class="text-xs text-slate-500">
+        <p class="text-xs text-slate-500 dark:text-slate-400">
             Numeric inputs snap to 0.5 (days) or 0.05 (RTB) on blur. Edits save automatically
             Numeric inputs snap to 0.5 (days) or 0.05 (RTB) on blur. Edits save automatically
             with a 400&nbsp;ms debounce; Available turns red if a worker is overcommitted.
             with a 400&nbsp;ms debounce; Available turns red if a worker is overcommitted.
         </p>
         </p>
 
 
     <!-- Section B: Task list -->
     <!-- Section B: Task list -->
-    <section class="rounded-lg border bg-white overflow-hidden"
+    <section class="rounded-lg border bg-white overflow-hidden dark:bg-slate-800 dark:border-slate-700"
              data-task-section>
              data-task-section>
-        <div class="px-4 py-3 border-b bg-slate-50 flex flex-wrap items-center gap-2">
-            <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Tasks</h2>
+        <div class="px-4 py-3 border-b bg-slate-50 flex flex-wrap items-center gap-2 dark:bg-slate-700 dark:border-slate-700">
+            <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Tasks</h2>
 
 
             <!-- Toolbar -->
             <!-- Toolbar -->
             <div class="ml-auto flex flex-wrap items-center gap-2">
             <div class="ml-auto flex flex-wrap items-center gap-2">
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <button type="button" data-reset-filters
                 <button type="button" data-reset-filters
-                        class="hidden rounded border border-slate-300 px-2 py-1 text-sm bg-white text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                        class="hidden rounded border border-slate-300 px-2 py-1 text-sm bg-white text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                     Reset
                     Reset
                 </button>
                 </button>
 
 
                 <input type="search" data-task-search placeholder="Search…"
                 <input type="search" data-task-search placeholder="Search…"
-                       class="rounded border border-slate-300 px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="rounded border border-slate-300 px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
 
 
                 <select data-prio-filter
                 <select data-prio-filter
-                        class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                        class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                     <option value="">All prios</option>
                     <option value="">All prios</option>
                     <option value="1">Prio 1 only</option>
                     <option value="1">Prio 1 only</option>
                     <option value="2">Prio 2 only</option>
                     <option value="2">Prio 2 only</option>
@@ -244,26 +244,26 @@ if (!function_exists('fmt_days')) {
                 <!-- Multi-select owner filter -->
                 <!-- Multi-select owner filter -->
                 <div class="relative" data-owner-filter-root>
                 <div class="relative" data-owner-filter-root>
                     <button type="button" data-owner-filter-trigger
                     <button type="button" data-owner-filter-trigger
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
-                        Owners <span data-owner-filter-count class="text-slate-500"></span>
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
+                        Owners <span data-owner-filter-count class="text-slate-500 dark:text-slate-400"></span>
                     </button>
                     </button>
                     <div data-owner-filter-dropdown
                     <div data-owner-filter-dropdown
-                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg">
-                        <div class="px-3 py-2 text-xs text-slate-500 flex items-center justify-between">
+                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg dark:bg-slate-800 dark:border-slate-700">
+                        <div class="px-3 py-2 text-xs text-slate-500 flex items-center justify-between dark:text-slate-400">
                             <span>Owner</span>
                             <span>Owner</span>
                             <button type="button" data-owner-filter-clear
                             <button type="button" data-owner-filter-clear
-                                    class="text-blue-700 hover:underline">Clear</button>
+                                    class="text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300">Clear</button>
                         </div>
                         </div>
                         <div class="max-h-64 overflow-y-auto">
                         <div class="max-h-64 overflow-y-auto">
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
                                 <input type="checkbox" data-owner-filter-opt value="__none__"
                                 <input type="checkbox" data-owner-filter-opt value="__none__"
-                                       class="rounded border-slate-300">
-                                <span class="text-slate-500 italic">No owner</span>
+                                       class="rounded border-slate-300 dark:border-slate-600">
+                                <span class="text-slate-500 italic dark:text-slate-400">No owner</span>
                             </label>
                             </label>
                             <?php foreach ($ownerChoices as $ow): ?>
                             <?php foreach ($ownerChoices as $ow): ?>
-                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
+                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
                                     <input type="checkbox" data-owner-filter-opt value="<?= (int) $ow->id ?>"
                                     <input type="checkbox" data-owner-filter-opt value="<?= (int) $ow->id ?>"
-                                           class="rounded border-slate-300">
+                                           class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($ow->name) ?></span>
                                     <span><?= e($ow->name) ?></span>
                                 </label>
                                 </label>
                             <?php endforeach; ?>
                             <?php endforeach; ?>
@@ -275,9 +275,9 @@ if (!function_exists('fmt_days')) {
                      assignment is 0 and collapses worker columns that are
                      assignment is 0 and collapses worker columns that are
                      all-zero for the remaining rows. -->
                      all-zero for the remaining rows. -->
                 <div class="flex items-center gap-1" data-focus-filter-root>
                 <div class="flex items-center gap-1" data-focus-filter-root>
-                    <label for="data-focus-select" class="text-xs uppercase tracking-wider text-slate-500">Focus</label>
+                    <label for="data-focus-select" class="text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400">Focus</label>
                     <select id="data-focus-select" data-focus-select
                     <select id="data-focus-select" data-focus-select
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                         <option value="">All workers</option>
                         <option value="">All workers</option>
                         <?php foreach ($sprintWorkers as $sw): ?>
                         <?php foreach ($sprintWorkers as $sw): ?>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
@@ -288,28 +288,28 @@ if (!function_exists('fmt_days')) {
                 <!-- Column visibility -->
                 <!-- Column visibility -->
                 <div class="relative" data-columns-root>
                 <div class="relative" data-columns-root>
                     <button type="button" data-columns-trigger
                     <button type="button" data-columns-trigger
-                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                            class="rounded border border-slate-300 px-2 py-1 text-sm bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                         Columns
                         Columns
                     </button>
                     </button>
                     <div data-columns-dropdown
                     <div data-columns-dropdown
-                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg">
-                        <div class="px-3 py-2 text-xs text-slate-500">Show columns</div>
+                         class="hidden absolute right-0 z-20 mt-1 w-56 rounded-md border border-slate-200 bg-white shadow-lg dark:bg-slate-800 dark:border-slate-700">
+                        <div class="px-3 py-2 text-xs text-slate-500 dark:text-slate-400">Show columns</div>
                         <div class="max-h-64 overflow-y-auto">
                         <div class="max-h-64 overflow-y-auto">
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="owner" checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="owner" checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Owner</span>
                                 <span>Owner</span>
                             </label>
                             </label>
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="prio"  checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="prio"  checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Prio</span>
                                 <span>Prio</span>
                             </label>
                             </label>
-                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                <input type="checkbox" data-column-opt value="tot"   checked class="rounded border-slate-300">
+                            <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                <input type="checkbox" data-column-opt value="tot"   checked class="rounded border-slate-300 dark:border-slate-600">
                                 <span>Tot</span>
                                 <span>Tot</span>
                             </label>
                             </label>
                             <?php foreach ($sprintWorkers as $sw): ?>
                             <?php foreach ($sprintWorkers as $sw): ?>
-                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50">
-                                    <input type="checkbox" data-column-opt value="sw-<?= (int) $sw->id ?>" checked class="rounded border-slate-300">
+                                <label class="flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-700">
+                                    <input type="checkbox" data-column-opt value="sw-<?= (int) $sw->id ?>" checked class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($sw->workerName) ?></span>
                                     <span><?= e($sw->workerName) ?></span>
                                 </label>
                                 </label>
                             <?php endforeach; ?>
                             <?php endforeach; ?>
@@ -319,7 +319,7 @@ if (!function_exists('fmt_days')) {
 
 
                 <?php if ($currentUser->isAdmin): ?>
                 <?php if ($currentUser->isAdmin): ?>
                     <button type="button" data-add-task
                     <button type="button" data-add-task
-                            class="rounded bg-slate-900 text-white px-3 py-1 text-sm font-medium hover:bg-slate-800">
+                            class="rounded bg-slate-900 text-white px-3 py-1 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                         + Add task
                         + Add task
                     </button>
                     </button>
                 <?php endif; ?>
                 <?php endif; ?>
@@ -328,7 +328,7 @@ if (!function_exists('fmt_days')) {
 
 
         <div class="overflow-x-auto">
         <div class="overflow-x-auto">
             <table class="min-w-full text-sm" data-task-table>
             <table class="min-w-full text-sm" data-task-table>
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="w-6 px-2 py-2"></th>
                         <th class="w-6 px-2 py-2"></th>
                         <th class="text-left px-2 py-2 font-semibold cursor-pointer select-none"
                         <th class="text-left px-2 py-2 font-semibold cursor-pointer select-none"
@@ -349,10 +349,10 @@ if (!function_exists('fmt_days')) {
                         <th class="w-8 px-2 py-2"></th>
                         <th class="w-8 px-2 py-2"></th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100" data-task-tbody>
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700" data-task-tbody>
                     <?php if ($tasks === []): ?>
                     <?php if ($tasks === []): ?>
                         <tr data-empty-tasks>
                         <tr data-empty-tasks>
-                            <td colspan="<?= 6 + count($sprintWorkers) ?>" class="px-3 py-8 text-center text-slate-500 text-sm">
+                            <td colspan="<?= 6 + count($sprintWorkers) ?>" class="px-3 py-8 text-center text-slate-500 text-sm dark:text-slate-400">
                                 No tasks yet.
                                 No tasks yet.
                                 <?php if ($currentUser->isAdmin): ?>
                                 <?php if ($currentUser->isAdmin): ?>
                                     Click <b>+ Add task</b> to start.
                                     Click <b>+ Add task</b> to start.
@@ -369,14 +369,14 @@ if (!function_exists('fmt_days')) {
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 <td class="px-2 py-1">
                                 <td class="px-2 py-1">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
-                                        <span class="handle cursor-grab text-slate-400 select-none">&#8801;</span>
+                                        <span class="handle cursor-grab text-slate-400 select-none dark:text-slate-500">&#8801;</span>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                                 <td class="px-2 py-1 min-w-[14rem]">
                                 <td class="px-2 py-1 min-w-[14rem]">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <input type="text" data-title
                                         <input type="text" data-title
                                                value="<?= e($t->title) ?>"
                                                value="<?= e($t->title) ?>"
-                                               class="w-full rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                               class="w-full rounded border border-slate-200 px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                     <?php else: ?>
                                     <?php else: ?>
                                         <span><?= e($t->title) ?></span>
                                         <span><?= e($t->title) ?></span>
                                     <?php endif; ?>
                                     <?php endif; ?>
@@ -384,7 +384,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1" data-col="owner">
                                 <td class="px-2 py-1" data-col="owner">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <select data-owner-select
                                         <select data-owner-select
-                                                class="w-full rounded border border-slate-200 px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                class="w-full rounded border border-slate-200 px-2 py-1 bg-white focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                             <option value="">—</option>
                                             <option value="">—</option>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                                 <option value="<?= (int) $ow->id ?>" <?= $t->ownerWorkerId === $ow->id ? 'selected' : '' ?>>
                                                 <option value="<?= (int) $ow->id ?>" <?= $t->ownerWorkerId === $ow->id ? 'selected' : '' ?>>
@@ -405,7 +405,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1 text-center" data-col="prio">
                                 <td class="px-2 py-1 text-center" data-col="prio">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <select data-prio-select
                                         <select data-prio-select
-                                                class="rounded border border-slate-200 px-2 py-1 bg-white font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                class="rounded border border-slate-200 px-2 py-1 bg-white font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                             <option value="1" <?= $t->priority === 1 ? 'selected' : '' ?>>1</option>
                                             <option value="1" <?= $t->priority === 1 ? 'selected' : '' ?>>1</option>
                                             <option value="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                             <option value="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                         </select>
                                         </select>
@@ -426,7 +426,7 @@ if (!function_exists('fmt_days')) {
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    data-assign
                                                    data-assign
                                                    data-sw-id="<?= (int) $sw->id ?>"
                                                    data-sw-id="<?= (int) $sw->id ?>"
-                                                   class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                                   class="w-14 rounded border border-slate-200 px-1 py-1 text-center font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                         <?php else: ?>
                                         <?php else: ?>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                         <?php endif; ?>
                                         <?php endif; ?>
@@ -435,7 +435,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-1 py-1 text-right">
                                 <td class="px-1 py-1 text-right">
                                     <?php if ($currentUser->isAdmin): ?>
                                     <?php if ($currentUser->isAdmin): ?>
                                         <button type="button" data-delete-task
                                         <button type="button" data-delete-task
-                                                class="text-sm text-red-600 hover:underline">×</button>
+                                                class="text-sm text-red-600 hover:underline dark:text-red-400">×</button>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                             </tr>
                             </tr>
@@ -444,7 +444,7 @@ if (!function_exists('fmt_days')) {
                 </tbody>
                 </tbody>
             </table>
             </table>
         </div>
         </div>
-        <div data-task-empty-filter class="hidden p-4 text-center text-slate-500 text-sm">
+        <div data-task-empty-filter class="hidden p-4 text-center text-slate-500 text-sm dark:text-slate-400">
             No tasks match the current filters.
             No tasks match the current filters.
         </div>
         </div>
     </section>
     </section>

+ 13 - 13
views/users/index.php

@@ -21,7 +21,7 @@ $flashMessages = [
 <section class="space-y-6">
 <section class="space-y-6">
     <div>
     <div>
         <h1 class="text-2xl font-semibold tracking-tight">Users</h1>
         <h1 class="text-2xl font-semibold tracking-tight">Users</h1>
-        <p class="text-slate-600 text-sm mt-1 max-w-prose">
+        <p class="text-slate-600 text-sm mt-1 max-w-prose dark:text-slate-400">
             Everyone who has ever signed in. Toggle admin status here; you
             Everyone who has ever signed in. Toggle admin status here; you
             cannot demote yourself or the last admin. Users are never deleted
             cannot demote yourself or the last admin. Users are never deleted
             — inactive accounts simply stop signing in.
             — inactive accounts simply stop signing in.
@@ -29,22 +29,22 @@ $flashMessages = [
     </div>
     </div>
 
 
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
-        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
+        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:bg-red-900 dark:border-red-800 dark:text-red-200">
             <?= e($errorMessages[$error]) ?>
             <?= e($errorMessages[$error]) ?>
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
     <?php if ($flash !== '' && isset($flashMessages[$flash])): ?>
     <?php if ($flash !== '' && isset($flashMessages[$flash])): ?>
-        <div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">
+        <div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-900 dark:border-green-800 dark:text-green-200">
             <?= e($flashMessages[$flash]) ?>
             <?= e($flashMessages[$flash]) ?>
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
 
 
-    <div class="rounded-lg border bg-white overflow-hidden">
+    <div class="rounded-lg border bg-white overflow-hidden dark:bg-slate-800 dark:border-slate-700">
         <?php if ($users === []): ?>
         <?php if ($users === []): ?>
-            <div class="p-8 text-center text-slate-500 text-sm">No users yet.</div>
+            <div class="p-8 text-center text-slate-500 text-sm dark:text-slate-400">No users yet.</div>
         <?php else: ?>
         <?php else: ?>
             <table class="min-w-full text-sm">
             <table class="min-w-full text-sm">
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="text-left px-4 py-2 font-semibold">Email</th>
                         <th class="text-left px-4 py-2 font-semibold">Email</th>
                         <th class="text-left px-4 py-2 font-semibold">Display name</th>
                         <th class="text-left px-4 py-2 font-semibold">Display name</th>
@@ -53,7 +53,7 @@ $flashMessages = [
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <?php foreach ($users as $u): $isSelf = $u->id === $currentUser->id; ?>
                     <?php foreach ($users as $u): $isSelf = $u->id === $currentUser->id; ?>
                         <tr>
                         <tr>
                             <form method="post" action="/users/<?= (int) $u->id ?>" class="contents">
                             <form method="post" action="/users/<?= (int) $u->id ?>" class="contents">
@@ -61,25 +61,25 @@ $flashMessages = [
                                 <td class="px-4 py-2 font-mono text-xs">
                                 <td class="px-4 py-2 font-mono text-xs">
                                     <?= e($u->email) ?>
                                     <?= e($u->email) ?>
                                     <?php if ($isSelf): ?>
                                     <?php if ($isSelf): ?>
-                                        <span class="ml-1 inline-block px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-slate-100 text-slate-700 rounded">you</span>
+                                        <span class="ml-1 inline-block px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider bg-slate-100 text-slate-700 rounded dark:bg-slate-700 dark:text-slate-200">you</span>
                                     <?php endif; ?>
                                     <?php endif; ?>
                                 </td>
                                 </td>
                                 <td class="px-4 py-2"><?= e($u->displayName) ?></td>
                                 <td class="px-4 py-2"><?= e($u->displayName) ?></td>
-                                <td class="px-4 py-2 text-slate-500 font-mono text-xs">
-                                    <?= $u->lastLoginAt !== null ? e($u->lastLoginAt) : '<span class="text-slate-400">—</span>' ?>
+                                <td class="px-4 py-2 text-slate-500 font-mono text-xs dark:text-slate-400">
+                                    <?= $u->lastLoginAt !== null ? e($u->lastLoginAt) : '<span class="text-slate-400 dark:text-slate-500">—</span>' ?>
                                 </td>
                                 </td>
                                 <td class="px-4 py-2">
                                 <td class="px-4 py-2">
                                     <label class="inline-flex items-center gap-2">
                                     <label class="inline-flex items-center gap-2">
                                         <input name="is_admin" type="checkbox" value="1"
                                         <input name="is_admin" type="checkbox" value="1"
                                                <?= $u->isAdmin ? 'checked' : '' ?>
                                                <?= $u->isAdmin ? 'checked' : '' ?>
                                                <?= $isSelf && $u->isAdmin ? 'disabled title="You cannot demote yourself"' : '' ?>
                                                <?= $isSelf && $u->isAdmin ? 'disabled title="You cannot demote yourself"' : '' ?>
-                                               class="rounded border-slate-300">
-                                        <span class="text-slate-600">admin</span>
+                                               class="rounded border-slate-300 dark:border-slate-600">
+                                        <span class="text-slate-600 dark:text-slate-400">admin</span>
                                     </label>
                                     </label>
                                 </td>
                                 </td>
                                 <td class="px-4 py-2 text-right">
                                 <td class="px-4 py-2 text-right">
                                     <button type="submit"
                                     <button type="submit"
-                                            class="rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100">
+                                            class="rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                                         Save
                                         Save
                                     </button>
                                     </button>
                                 </td>
                                 </td>

+ 19 - 19
views/workers/index.php

@@ -20,7 +20,7 @@ $flashMessages = [
 <section class="space-y-6">
 <section class="space-y-6">
     <div>
     <div>
         <h1 class="text-2xl font-semibold tracking-tight">Workers</h1>
         <h1 class="text-2xl font-semibold tracking-tight">Workers</h1>
-        <p class="text-slate-600 mt-1 text-sm max-w-prose">
+        <p class="text-slate-600 mt-1 text-sm max-w-prose dark:text-slate-400">
             Master data for the people tasks get assigned to. Workers are not the
             Master data for the people tasks get assigned to. Workers are not the
             same as users &mdash; a worker doesn't have to ever sign in. To remove
             same as users &mdash; a worker doesn't have to ever sign in. To remove
             someone, toggle them inactive rather than deleting.
             someone, toggle them inactive rather than deleting.
@@ -28,45 +28,45 @@ $flashMessages = [
     </div>
     </div>
 
 
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
     <?php if ($error !== '' && isset($errorMessages[$error])): ?>
-        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
+        <div class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:bg-red-900 dark:border-red-800 dark:text-red-200">
             <?= e($errorMessages[$error]) ?>
             <?= e($errorMessages[$error]) ?>
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
     <?php if ($flash !== '' && isset($flashMessages[$flash])): ?>
     <?php if ($flash !== '' && isset($flashMessages[$flash])): ?>
-        <div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">
+        <div class="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:bg-green-900 dark:border-green-800 dark:text-green-200">
             <?= e($flashMessages[$flash]) ?>
             <?= e($flashMessages[$flash]) ?>
         </div>
         </div>
     <?php endif; ?>
     <?php endif; ?>
 
 
     <!-- Add worker -->
     <!-- Add worker -->
-    <div class="rounded-lg border bg-white p-4">
-        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider">Add worker</h2>
+    <div class="rounded-lg border bg-white p-4 dark:bg-slate-800 dark:border-slate-700">
+        <h2 class="text-sm font-semibold text-slate-700 uppercase tracking-wider dark:text-slate-200">Add worker</h2>
         <form method="post" action="/workers" class="mt-3 flex flex-wrap items-end gap-3">
         <form method="post" action="/workers" class="mt-3 flex flex-wrap items-end gap-3">
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
             <label class="flex-1 min-w-[12rem]">
             <label class="flex-1 min-w-[12rem]">
-                <span class="text-xs text-slate-600">Name</span>
+                <span class="text-xs text-slate-600 dark:text-slate-400">Name</span>
                 <input name="name" type="text" required
                 <input name="name" type="text" required
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <label class="w-36">
             <label class="w-36">
-                <span class="text-xs text-slate-600">Default RTB (0–1)</span>
+                <span class="text-xs text-slate-600 dark:text-slate-400">Default RTB (0–1)</span>
                 <input name="default_rtb" type="number" min="0" max="1" step="0.05" value="0.00"
                 <input name="default_rtb" type="number" min="0" max="1" step="0.05" value="0.00"
-                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                       class="mt-1 block w-full rounded-md border-slate-300 border shadow-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
             </label>
             </label>
             <button type="submit"
             <button type="submit"
-                    class="rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800">
+                    class="rounded-md bg-slate-900 text-white px-4 py-2 text-sm font-medium hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600">
                 Add
                 Add
             </button>
             </button>
         </form>
         </form>
     </div>
     </div>
 
 
     <!-- Workers table -->
     <!-- Workers table -->
-    <div class="rounded-lg border bg-white overflow-hidden">
+    <div class="rounded-lg border bg-white overflow-hidden dark:bg-slate-800 dark:border-slate-700">
         <?php if ($workers === []): ?>
         <?php if ($workers === []): ?>
-            <div class="p-8 text-center text-slate-500 text-sm">No workers yet.</div>
+            <div class="p-8 text-center text-slate-500 text-sm dark:text-slate-400">No workers yet.</div>
         <?php else: ?>
         <?php else: ?>
             <table class="min-w-full text-sm">
             <table class="min-w-full text-sm">
-                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
+                <thead class="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider dark:bg-slate-700 dark:text-slate-300">
                     <tr>
                     <tr>
                         <th class="text-left px-4 py-2 font-semibold">Name</th>
                         <th class="text-left px-4 py-2 font-semibold">Name</th>
                         <th class="text-left px-4 py-2 font-semibold">Default RTB</th>
                         <th class="text-left px-4 py-2 font-semibold">Default RTB</th>
@@ -74,7 +74,7 @@ $flashMessages = [
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <?php foreach ($workers as $w): ?>
                     <?php foreach ($workers as $w): ?>
                         <tr class="<?= $w->isActive ? '' : 'opacity-60' ?>">
                         <tr class="<?= $w->isActive ? '' : 'opacity-60' ?>">
                             <form method="post" action="/workers/<?= (int) $w->id ?>" class="contents">
                             <form method="post" action="/workers/<?= (int) $w->id ?>" class="contents">
@@ -82,23 +82,23 @@ $flashMessages = [
                                 <td class="px-4 py-2">
                                 <td class="px-4 py-2">
                                     <input name="name" type="text" required
                                     <input name="name" type="text" required
                                            value="<?= e($w->name) ?>"
                                            value="<?= e($w->name) ?>"
-                                           class="w-full rounded-md border-slate-300 border shadow-sm px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                           class="w-full rounded-md border-slate-300 border shadow-sm px-2 py-1 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                 </td>
                                 </td>
                                 <td class="px-4 py-2 w-32">
                                 <td class="px-4 py-2 w-32">
                                     <input name="default_rtb" type="number" min="0" max="1" step="0.05"
                                     <input name="default_rtb" type="number" min="0" max="1" step="0.05"
                                            value="<?= e(number_format($w->defaultRtb, 2, '.', '')) ?>"
                                            value="<?= e(number_format($w->defaultRtb, 2, '.', '')) ?>"
-                                           class="w-full rounded-md border-slate-300 border shadow-sm px-2 py-1 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400">
+                                           class="w-full rounded-md border-slate-300 border shadow-sm px-2 py-1 font-mono focus:outline-none focus:ring-2 focus:ring-slate-400 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100 dark:focus:ring-slate-500">
                                 </td>
                                 </td>
                                 <td class="px-4 py-2">
                                 <td class="px-4 py-2">
                                     <label class="inline-flex items-center gap-2">
                                     <label class="inline-flex items-center gap-2">
                                         <input name="is_active" type="checkbox" value="1" <?= $w->isActive ? 'checked' : '' ?>
                                         <input name="is_active" type="checkbox" value="1" <?= $w->isActive ? 'checked' : '' ?>
-                                               class="rounded border-slate-300">
-                                        <span class="text-slate-600">active</span>
+                                               class="rounded border-slate-300 dark:border-slate-600">
+                                        <span class="text-slate-600 dark:text-slate-400">active</span>
                                     </label>
                                     </label>
                                 </td>
                                 </td>
                                 <td class="px-4 py-2 text-right">
                                 <td class="px-4 py-2 text-right">
                                     <button type="submit"
                                     <button type="submit"
-                                            class="rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100">
+                                            class="rounded-md border border-slate-300 bg-white text-slate-700 px-3 py-1 text-sm hover:bg-slate-100 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-700">
                                         Save
                                         Save
                                     </button>
                                     </button>
                                 </td>
                                 </td>