Ver Fonte

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 há 2 semanas atrás
pai
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
      `[sprint-planner] beamer: table still overflows …` and
      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); }
     });
 })();
+
+/**
+ * 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} */
 module.exports = {
+    darkMode: 'class',
     content: [
         './views/**/*.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">
         <div>
             <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' ?>
                 · page <?= (int) $page ?> / <?= (int) $pages ?>
                 · <?= (int) $pageSize ?> per page
@@ -44,11 +44,11 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
 
     <!-- Filter form -->
     <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">
-            <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"
-                    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>
                 <?php foreach ($users as $u): ?>
                     <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 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"
-                    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>
                 <?php foreach ($actions as $a): ?>
                     <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 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"
-                    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>
                 <?php foreach ($entityTypes as $t): ?>
                     <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 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"
                    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 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"
                    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 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"
                    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>
 
         <div class="md:col-span-6 flex items-center justify-end gap-2">
             <?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; ?>
             <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
             </button>
         </div>
     </form>
 
     <!-- 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 === []): ?>
-            <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: ?>
             <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>
                         <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>
@@ -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>
                     </tr>
                 </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): ?>
                         <tr>
                             <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">
                                 <?= $r['user_email'] !== null && $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 class="px-3 py-2">
                                 <span class="inline-block px-1.5 py-0.5 rounded text-xs font-mono
                                     <?php
                                     $action = (string) $r['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) ?>
@@ -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">
                                 <?= e((string) $r['entity_type']) ?>
                                 <?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']) ?>
                                 <?php endif; ?>
                             </td>
                             <td class="px-3 py-2">
                                 <?php $b = $prettyJson($r['before_json'] ?? null); $a = $prettyJson($r['after_json'] ?? null); ?>
                                 <?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: ?>
                                     <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 !== '' ? 'before only' : 'after only') ?>
                                         </summary>
                                         <?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 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; ?>
                                     </details>
                                 <?php endif; ?>
                             </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'] ?? '')) ?>
                                 <?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>
                                 <?php endif; ?>
                             </td>
@@ -205,12 +205,12 @@ $anyFilter = array_filter($filters, fn($v) => $v !== '' && $v !== null);
             $nextQs = $qsWithout($filters, 'page', ['page' => min($pages, $page + 1)]);
             ?>
             <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
             </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) ?>"
-               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 →
             </a>
         </nav>

+ 9 - 9
views/auth/local.php

@@ -5,15 +5,15 @@
 use function App\Http\e;
 ?>
 <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>
-        <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
             come from the <code>LOCAL_ADMIN_*</code> environment variables.
         </p>
 
         <?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.
             </div>
         <?php endif; ?>
@@ -23,29 +23,29 @@ use function App\Http\e;
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
 
             <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
                        value="<?= e($email) ?>"
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                               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 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
                        class="mt-1 block w-full rounded-md border-slate-300 shadow-sm
                               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>
 
             <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
             </button>
         </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>
         </p>
     </div>

+ 27 - 27
views/home.php

@@ -12,33 +12,33 @@ $sprintRows = $sprintRows ?? [];
 ?>
 <section class="space-y-6">
     <?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.
         </div>
     <?php endif; ?>
 
     <?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>
-            <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
                 to sign in becomes the admin automatically.
             </p>
             <div class="mt-4 flex flex-wrap items-center gap-3">
                 <?php if ($oidcConfigured): ?>
                     <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
                     </a>
                 <?php endif; ?>
                 <?php if ($localAdminEnabled): ?>
                     <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
                     </a>
                 <?php endif; ?>
                 <?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
                         <code>LOCAL_ADMIN_*</code> in <code>.env</code>.
                     </span>
@@ -49,29 +49,29 @@ $sprintRows = $sprintRows ?? [];
         <div class="flex items-end justify-between gap-4">
             <div>
                 <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' ?>.
                 </p>
             </div>
             <?php if ($currentUser->isAdmin): ?>
                 <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
                 </a>
             <?php endif; ?>
         </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 === []): ?>
-                <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.
                     <?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; ?>
                 </div>
             <?php else: ?>
                 <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>
                             <th class="text-left px-4 py-2 font-semibold">Name</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>
                         </tr>
                     </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']; ?>
-                            <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 ?>">
                                 <td class="px-4 py-2 font-medium">
                                     <a href="/sprints/<?= (int) $s->id ?>" class="hover:underline">
                                         <?= e($s->name) ?>
                                     </a>
                                 </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) ?>
                                 </td>
                                 <td class="px-4 py-2 text-right font-mono"><?= (int) $row['nWorkers'] ?></td>
@@ -100,9 +100,9 @@ $sprintRows = $sprintRows ?? [];
                                 </td>
                                 <td class="px-4 py-2">
                                     <?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: ?>
-                                        <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; ?>
                                 </td>
                             </tr>
@@ -114,29 +114,29 @@ $sprintRows = $sprintRows ?? [];
     <?php endif; ?>
 
     <?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">
-            <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>
 
-            <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>
 
-            <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>
 
-            <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>
 
-            <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>
 
-            <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>
         </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>
     </details>
     <?php endif; ?>

+ 21 - 15
views/layout.php

@@ -13,6 +13,7 @@ $csrfToken   = $csrfToken   ?? '';
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <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="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="/assets/js/app.js" defer></script>
 </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">
             <a href="/" class="font-semibold tracking-tight">Sprint Planner</a>
 
             <nav class="ml-auto flex items-center gap-4 text-sm">
                 <?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): ?>
-                        <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; ?>
-                    <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) ?>
                         <?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; ?>
                     </span>
                     <div class="relative">
@@ -45,7 +46,7 @@ $csrfToken   = $csrfToken   ?? '';
                                 aria-haspopup="true"
                                 aria-controls="app-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">
                                 <line x1="3" y1="5"  x2="17" y2="5"></line>
                                 <line x1="3" y1="10" x2="17" y2="10"></line>
@@ -56,20 +57,25 @@ $csrfToken   = $csrfToken   ?? '';
                              data-menu
                              role="menu"
                              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): ?>
                                 <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"
-                                   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"
-                                   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; ?>
+                            <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">
                                 <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
                                 <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
                                 </button>
                             </form>
@@ -77,7 +83,7 @@ $csrfToken   = $csrfToken   ?? '';
                     </div>
                 <?php else: ?>
                     <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; ?>
             </nav>
         </div>

+ 17 - 17
views/sprints/new.php

@@ -17,66 +17,66 @@ $errorMessages = [
 ?>
 <section class="max-w-xl">
     <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
         sprint page after creation.
     </p>
 
     <?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]) ?>
         </div>
     <?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) ?>">
 
         <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
                    value="<?= e($form['name']) ?>"
                    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>
 
         <div class="grid grid-cols-2 gap-3">
             <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
                        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 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
                        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>
         </div>
 
         <div class="grid grid-cols-2 gap-3">
             <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
                        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 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
                        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>
         </div>
 
         <div class="flex gap-3 pt-2">
             <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
             </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
             </a>
         </div>

+ 48 - 47
views/sprints/present.php

@@ -42,6 +42,7 @@ if (!function_exists('fmt_days')) {
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width,initial-scale=1">
     <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="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="/assets/js/sprint-planner.js" defer></script>
 </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"
       data-sprint-root
       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-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">
             <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) ?>
                 <?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; ?>
             </p>
         </div>
         <div class="flex items-center gap-3">
             <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>
             <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
             </a>
         </div>
     </header>
 
     <?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 === []): ?>
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
             <?php elseif ($sprintWorkers === []): ?>
@@ -91,24 +92,24 @@ if (!function_exists('fmt_days')) {
     <!-- Task list — same structure as views/sprints/show.php's
          [data-task-section]. sprint-planner.js wires everything from the
          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>
-        <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 -->
             <div class="ml-auto flex flex-wrap items-center gap-2">
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <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
                 </button>
 
                 <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
-                        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="1">Prio 1 only</option>
                     <option value="2">Prio 2 only</option>
@@ -117,26 +118,26 @@ if (!function_exists('fmt_days')) {
                 <!-- Multi-select owner filter -->
                 <div class="relative" data-owner-filter-root>
                     <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>
                     <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>
                             <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 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__"
-                                       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>
                             <?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 ?>"
-                                           class="rounded border-slate-300">
+                                           class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($ow->name) ?></span>
                                 </label>
                             <?php endforeach; ?>
@@ -146,9 +147,9 @@ if (!function_exists('fmt_days')) {
 
                 <!-- Focus filter -->
                 <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
-                            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>
                         <?php foreach ($sprintWorkers as $sw): ?>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
@@ -159,28 +160,28 @@ if (!function_exists('fmt_days')) {
                 <!-- Column visibility -->
                 <div class="relative" data-columns-root>
                     <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
                     </button>
                     <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">
-                            <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>
                             </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>
                             </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>
                             </label>
                             <?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>
                                 </label>
                             <?php endforeach; ?>
@@ -190,7 +191,7 @@ if (!function_exists('fmt_days')) {
 
                 <?php if ($currentUser->isAdmin): ?>
                     <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
                     </button>
                 <?php endif; ?>
@@ -199,7 +200,7 @@ if (!function_exists('fmt_days')) {
 
         <div class="overflow-x-auto">
             <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>
                         <th class="w-6 px-2 py-2"></th>
                         <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>
                     </tr>
                 </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 === []): ?>
                         <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.
                                 <?php if ($currentUser->isAdmin): ?>
                                     Click <b>+ Add task</b> to start.
@@ -240,14 +241,14 @@ if (!function_exists('fmt_days')) {
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 <td class="px-2 py-1">
                                     <?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; ?>
                                 </td>
                                 <td class="px-2 py-1 min-w-[14rem]">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <input type="text" data-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: ?>
                                         <span><?= e($t->title) ?></span>
                                     <?php endif; ?>
@@ -255,7 +256,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1" data-col="owner">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                                 <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">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                         </select>
@@ -297,7 +298,7 @@ if (!function_exists('fmt_days')) {
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    data-assign
                                                    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: ?>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                         <?php endif; ?>
@@ -306,7 +307,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-1 py-1 text-right">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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; ?>
                                 </td>
                             </tr>
@@ -315,7 +316,7 @@ if (!function_exists('fmt_days')) {
                 </tbody>
             </table>
         </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.
         </div>
     </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">
         <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="/sprints/<?= (int) $sprint->id ?>" class="hover:underline"><?= e($sprint->name) ?></a> /
             </nav>
             <h1 class="text-2xl font-semibold tracking-tight">Settings</h1>
         </div>
         <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>
     </header>
 
     <!-- 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">
             <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) ?>"
-                       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 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) ?>"
-                       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 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) ?>"
-                       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 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"
                        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>
         </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>
 
     <!-- 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>
-                <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' ?>.
                     Tick the weekdays that are workdays for each week; the count feeds
                     the Arbeitstage header on the sprint page.
@@ -68,20 +68,20 @@ use function App\Http\e;
             </div>
             <form data-weeks-form class="flex items-end gap-2">
                 <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"
                            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>
                 <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
                 </button>
             </form>
         </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>
-                <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>
                         <th class="text-left px-3 py-2 font-semibold">#</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>
                     </tr>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <?php foreach ($weeks as $w): ?>
                         <tr data-week-row data-week-id="<?= (int) $w->id ?>">
                             <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) ?>"
                                            aria-label="<?= e($label) ?>"
                                            <?= $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>
                             <?php endforeach; ?>
                             <td class="px-3 py-2 font-mono text-right" data-week-count>
@@ -117,61 +117,61 @@ use function App\Http\e;
                 </tbody>
             </table>
         </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.
         </p>
     </section>
 
     <!-- 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>
-                <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): ?>
-                            <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 ?>">
                                 <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>
                         <?php endforeach; ?>
                     </ul>
                     <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"' ?>>
                         No other active workers.
                     </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): ?>
-                            <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-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>
                                 <input type="number" step="0.05" min="0" max="1"
                                        value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                        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>
                         <?php endforeach; ?>
                     </ul>
                     <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"' ?>>
                         No workers assigned yet.
                     </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.
         </p>
     </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">
         <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> /
             </nav>
             <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) ?>
                 · Reserve <?= e(number_format($sprint->reserveFraction * 100, 0)) ?>%
                 <?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; ?>
             </p>
         </div>
         <div class="flex items-center gap-3">
             <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>
             <a href="/sprints/<?= (int) $sprint->id ?>/present"
                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
             </a>
             <?php if ($currentUser->isAdmin): ?>
                 <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
                 </a>
             <?php endif; ?>
@@ -65,7 +65,7 @@ if (!function_exists('fmt_days')) {
     </header>
 
     <?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 === []): ?>
                 No weeks yet. <a href="/sprints/<?= (int) $sprint->id ?>/settings" class="underline">Open settings</a> to add some.
             <?php elseif ($sprintWorkers === []): ?>
@@ -75,29 +75,29 @@ if (!function_exists('fmt_days')) {
     <?php else: ?>
 
         <!-- 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>
-                <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>
-                        <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): ?>
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                                 <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>
                         <?php endforeach; ?>
                         <th class="text-center px-2 py-2 font-semibold">Σ</th>
                         <th class="text-center px-2 py-2 font-semibold">RTB</th>
                     </tr>
                 </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. -->
-                    <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
                             <?php if ($currentUser->isAdmin): ?>
                                 <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>
                             <?php endif; ?>
                         </th>
@@ -108,7 +108,7 @@ if (!function_exists('fmt_days')) {
                                      title="<?= e(implode(' ', $w->activeDays())) ?: '—' ?>">
                                     <?php foreach (SprintWeek::DAY_LABELS as $bit => $_label): ?>
                                         <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; ?>
                                 </div>
                             </td>
@@ -123,10 +123,10 @@ if (!function_exists('fmt_days')) {
                     <?php foreach ($sprintWorkers as $sw): ?>
                         <?php $rowDays = $grid[$sw->id] ?? []; $rowSum = array_sum($rowDays); ?>
                         <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">
                                     <?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; ?>
                                     <?= e($sw->workerName) ?>
                                 </span>
@@ -138,7 +138,7 @@ if (!function_exists('fmt_days')) {
                                                value="<?= e(fmt_days($v)) ?>"
                                                data-day data-sw-id="<?= (int) $sw->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: ?>
                                         <span class="font-mono"><?= e(fmt_days($v)) ?></span>
                                     <?php endif; ?>
@@ -153,7 +153,7 @@ if (!function_exists('fmt_days')) {
                                     <input type="number" min="0" max="1" step="0.05"
                                            value="<?= e(number_format($sw->rtb, 2, '.', '')) ?>"
                                            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: ?>
                                     <span class="font-mono"><?= e(number_format($sw->rtb, 2, '.', '')) ?></span>
                                 <?php endif; ?>
@@ -165,14 +165,14 @@ if (!function_exists('fmt_days')) {
         </section>
 
         <!-- 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
             </div>
             <table class="min-w-full text-sm">
                 <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): ?>
                             <th class="text-center px-2 py-2 font-semibold whitespace-nowrap">
                                 <?= e($sw->workerName) ?>
@@ -180,9 +180,9 @@ if (!function_exists('fmt_days')) {
                         <?php endforeach; ?>
                     </tr>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <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; ?>
                             <td class="px-2 py-2 text-center font-mono"
                                 data-cap-ressourcen data-sw-id="<?= (int) $sw->id ?>">
@@ -191,18 +191,18 @@ if (!function_exists('fmt_days')) {
                         <?php endforeach; ?>
                     </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; ?>
-                            <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 ?>">
                                 <?= e(fmt_days($c['after_reserves'] ?? 0.0)) ?>
                             </td>
                         <?php endforeach; ?>
                     </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); ?>
-                            <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 ?>">
                                 <?= e(fmt_days($av)) ?>
                             </td>
@@ -212,30 +212,30 @@ if (!function_exists('fmt_days')) {
             </table>
         </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
             with a 400&nbsp;ms debounce; Available turns red if a worker is overcommitted.
         </p>
 
     <!-- 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>
-        <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 -->
             <div class="ml-auto flex flex-wrap items-center gap-2">
                 <!-- Reset (only visible while any filter is active — JS toggles the hidden class) -->
                 <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
                 </button>
 
                 <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
-                        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="1">Prio 1 only</option>
                     <option value="2">Prio 2 only</option>
@@ -244,26 +244,26 @@ if (!function_exists('fmt_days')) {
                 <!-- Multi-select owner filter -->
                 <div class="relative" data-owner-filter-root>
                     <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>
                     <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>
                             <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 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__"
-                                       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>
                             <?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 ?>"
-                                           class="rounded border-slate-300">
+                                           class="rounded border-slate-300 dark:border-slate-600">
                                     <span><?= e($ow->name) ?></span>
                                 </label>
                             <?php endforeach; ?>
@@ -275,9 +275,9 @@ if (!function_exists('fmt_days')) {
                      assignment is 0 and collapses worker columns that are
                      all-zero for the remaining rows. -->
                 <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
-                            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>
                         <?php foreach ($sprintWorkers as $sw): ?>
                             <option value="<?= (int) $sw->id ?>"><?= e($sw->workerName) ?></option>
@@ -288,28 +288,28 @@ if (!function_exists('fmt_days')) {
                 <!-- Column visibility -->
                 <div class="relative" data-columns-root>
                     <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
                     </button>
                     <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">
-                            <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>
                             </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>
                             </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>
                             </label>
                             <?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>
                                 </label>
                             <?php endforeach; ?>
@@ -319,7 +319,7 @@ if (!function_exists('fmt_days')) {
 
                 <?php if ($currentUser->isAdmin): ?>
                     <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
                     </button>
                 <?php endif; ?>
@@ -328,7 +328,7 @@ if (!function_exists('fmt_days')) {
 
         <div class="overflow-x-auto">
             <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>
                         <th class="w-6 px-2 py-2"></th>
                         <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>
                     </tr>
                 </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 === []): ?>
                         <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.
                                 <?php if ($currentUser->isAdmin): ?>
                                     Click <b>+ Add task</b> to start.
@@ -369,14 +369,14 @@ if (!function_exists('fmt_days')) {
                                 data-sort-order="<?= (int) $t->sortOrder ?>">
                                 <td class="px-2 py-1">
                                     <?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; ?>
                                 </td>
                                 <td class="px-2 py-1 min-w-[14rem]">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <input type="text" data-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: ?>
                                         <span><?= e($t->title) ?></span>
                                     <?php endif; ?>
@@ -384,7 +384,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-2 py-1" data-col="owner">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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>
                                             <?php foreach ($ownerChoices as $ow): ?>
                                                 <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">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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="2" <?= $t->priority === 2 ? 'selected' : '' ?>>2</option>
                                         </select>
@@ -426,7 +426,7 @@ if (!function_exists('fmt_days')) {
                                                    value="<?= e(fmt_days($d)) ?>"
                                                    data-assign
                                                    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: ?>
                                             <span class="font-mono"><?= e(fmt_days($d)) ?></span>
                                         <?php endif; ?>
@@ -435,7 +435,7 @@ if (!function_exists('fmt_days')) {
                                 <td class="px-1 py-1 text-right">
                                     <?php if ($currentUser->isAdmin): ?>
                                         <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; ?>
                                 </td>
                             </tr>
@@ -444,7 +444,7 @@ if (!function_exists('fmt_days')) {
                 </tbody>
             </table>
         </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.
         </div>
     </section>

+ 13 - 13
views/users/index.php

@@ -21,7 +21,7 @@ $flashMessages = [
 <section class="space-y-6">
     <div>
         <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
             cannot demote yourself or the last admin. Users are never deleted
             — inactive accounts simply stop signing in.
@@ -29,22 +29,22 @@ $flashMessages = [
     </div>
 
     <?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]) ?>
         </div>
     <?php endif; ?>
     <?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]) ?>
         </div>
     <?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 === []): ?>
-            <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: ?>
             <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>
                         <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>
@@ -53,7 +53,7 @@ $flashMessages = [
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                     </tr>
                 </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; ?>
                         <tr>
                             <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">
                                     <?= e($u->email) ?>
                                     <?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; ?>
                                 </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 class="px-4 py-2">
                                     <label class="inline-flex items-center gap-2">
                                         <input name="is_admin" type="checkbox" value="1"
                                                <?= $u->isAdmin ? 'checked' : '' ?>
                                                <?= $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>
                                 </td>
                                 <td class="px-4 py-2 text-right">
                                     <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
                                     </button>
                                 </td>

+ 19 - 19
views/workers/index.php

@@ -20,7 +20,7 @@ $flashMessages = [
 <section class="space-y-6">
     <div>
         <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
             same as users &mdash; a worker doesn't have to ever sign in. To remove
             someone, toggle them inactive rather than deleting.
@@ -28,45 +28,45 @@ $flashMessages = [
     </div>
 
     <?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]) ?>
         </div>
     <?php endif; ?>
     <?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]) ?>
         </div>
     <?php endif; ?>
 
     <!-- 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">
             <input type="hidden" name="_csrf" value="<?= e($csrfToken) ?>">
             <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
-                       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 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"
-                       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>
             <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
             </button>
         </form>
     </div>
 
     <!-- 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 === []): ?>
-            <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: ?>
             <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>
                         <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>
@@ -74,7 +74,7 @@ $flashMessages = [
                         <th class="text-right px-4 py-2 font-semibold">&nbsp;</th>
                     </tr>
                 </thead>
-                <tbody class="divide-y divide-slate-100">
+                <tbody class="divide-y divide-slate-100 dark:divide-slate-700">
                     <?php foreach ($workers as $w): ?>
                         <tr class="<?= $w->isActive ? '' : 'opacity-60' ?>">
                             <form method="post" action="/workers/<?= (int) $w->id ?>" class="contents">
@@ -82,23 +82,23 @@ $flashMessages = [
                                 <td class="px-4 py-2">
                                     <input name="name" type="text" required
                                            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 class="px-4 py-2 w-32">
                                     <input name="default_rtb" type="number" min="0" max="1" step="0.05"
                                            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 class="px-4 py-2">
                                     <label class="inline-flex items-center gap-2">
                                         <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>
                                 </td>
                                 <td class="px-4 py-2 text-right">
                                     <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
                                     </button>
                                 </td>