← Back to task

Commit 555d5d41

commit 555d5d416e2001d04145b173aa40fd40f656509d
Author: Ben Sima <ben@bensima.com>
Date:   Wed Nov 26 13:03:17 2025

    Add mobile-first CSS styling
    
    All checks pass. The Style.hs file was already implemented from
    a previo
    
    - Mobile-first CSS with Clay - Status badges with colored pills
    (Open=yellow, InProgress=blue, Review - Large touch targets (44px min
    height) - Single column layout on narrow screens (<600px) - Card-style
    sections with subtle shadows - Responsive navigation header - Dark
    mode support - Served at GET /style.css
    
    Task-Id: t-1o2g8gugkr1.9

diff --git a/Omni/Jr/Web.hs b/Omni/Jr/Web.hs
index c32c716c..69162aa6 100644
--- a/Omni/Jr/Web.hs
+++ b/Omni/Jr/Web.hs
@@ -9,6 +9,7 @@
 -- : dep servant-lucid
 -- : dep http-api-data
 -- : dep process
+-- : dep clay
 module Omni.Jr.Web
   ( run,
     defaultPort,
@@ -18,8 +19,11 @@ where
 import Alpha
 import qualified Data.List as List
 import qualified Data.Text as Text
+import qualified Data.Text.Lazy as LazyText
+import qualified Data.Text.Lazy.Encoding as LazyText
 import qualified Lucid
 import qualified Network.Wai.Handler.Warp as Warp
+import qualified Omni.Jr.Web.Style as Style
 import qualified Omni.Task.Core as TaskCore
 import Servant
 import qualified Servant.HTML.Lucid as Lucid
@@ -41,6 +45,7 @@ data TaskFilters = TaskFilters
 
 type API =
   Get '[Lucid.HTML] HomePage
+    :<|> "style.css" :> Get '[CSS] LazyText.Text
     :<|> "ready" :> Get '[Lucid.HTML] ReadyQueuePage
     :<|> "tasks"
       :> QueryParam "status" TaskCore.Status
@@ -53,6 +58,14 @@ type API =
     :<|> "tasks" :> Capture "id" Text :> "accept" :> PostRedirect
     :<|> "tasks" :> Capture "id" Text :> "reject" :> ReqBody '[FormUrlEncoded] RejectForm :> PostRedirect
 
+data CSS
+
+instance Accept CSS where
+  contentType _ = "text/css"
+
+instance MimeRender CSS LazyText.Text where
+  mimeRender _ = LazyText.encodeUtf8
+
 data HomePage = HomePage TaskCore.TaskStats [TaskCore.Task] [TaskCore.Task]
 
 newtype ReadyQueuePage = ReadyQueuePage [TaskCore.Task]
@@ -86,49 +99,77 @@ instance FromForm StatusForm where
       Just s -> Right (StatusForm s)
       Nothing -> Left "Invalid status"
 
+pageHead :: (Monad m) => Text -> Lucid.HtmlT m ()
+pageHead title =
+  Lucid.head_ <| do
+    Lucid.title_ (Lucid.toHtml title)
+    Lucid.meta_ [Lucid.charset_ "utf-8"]
+    Lucid.meta_
+      [ Lucid.name_ "viewport",
+        Lucid.content_ "width=device-width, initial-scale=1"
+      ]
+    Lucid.link_ [Lucid.rel_ "stylesheet", Lucid.href_ "/style.css"]
+
+statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
+statusBadge status =
+  let (cls, label) = case status of
+        TaskCore.Open -> ("badge badge-open", "Open")
+        TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
+        TaskCore.Review -> ("badge badge-review", "Review")
+        TaskCore.Approved -> ("badge badge-approved", "Approved")
+        TaskCore.Done -> ("badge badge-done", "Done")
+   in Lucid.span_ [Lucid.class_ cls] label
+
+renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
+renderTaskCard t =
+  Lucid.div_ [Lucid.class_ "task-card"] <| do
+    Lucid.div_ [Lucid.class_ "task-header"] <| do
+      Lucid.a_
+        [ Lucid.class_ "task-id",
+          Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
+        ]
+        (Lucid.toHtml (TaskCore.taskId t))
+      statusBadge (TaskCore.taskStatus t)
+      Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
+    Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
+
 instance Lucid.ToHtml HomePage where
   toHtmlRaw = Lucid.toHtml
   toHtml (HomePage stats readyTasks recentTasks) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ "Jr Dashboard"
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ homeStyles
+      pageHead "Jr Dashboard"
       Lucid.body_ <| do
-        Lucid.h1_ "Jr Dashboard"
-
-        Lucid.div_ [Lucid.class_ "actions"] <| do
-          Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "action-btn"] "View All Tasks"
-          Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "action-btn action-btn-primary"] "View Ready Queue"
-
-        Lucid.h2_ "Task Status"
-        Lucid.div_ [Lucid.class_ "stats-grid"] <| do
-          statCard "Open" (TaskCore.openTasks stats) "badge-open"
-          statCard "In Progress" (TaskCore.inProgressTasks stats) "badge-inprogress"
-          statCard "Review" (TaskCore.reviewTasks stats) "badge-review"
-          statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved"
-          statCard "Done" (TaskCore.doneTasks stats) "badge-done"
-
-        Lucid.h2_ <| do
-          "Ready Queue "
-          Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "ready-link"]
-            <| Lucid.toHtml ("(" <> tshow (length readyTasks) <> " tasks)")
-        if null readyTasks
-          then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks ready for work."
-          else
-            Lucid.div_ [Lucid.class_ "task-list"]
-              <| traverse_ renderTaskCard (take 5 readyTasks)
-
-        Lucid.h2_ "Recent Activity"
-        if null recentTasks
-          then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
-          else
-            Lucid.div_ [Lucid.class_ "task-list"]
-              <| traverse_ renderTaskCard recentTasks
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.h1_ "Jr Dashboard"
+
+          Lucid.div_ [Lucid.class_ "actions"] <| do
+            Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "action-btn"] "View All Tasks"
+            Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "action-btn action-btn-primary"] "View Ready Queue"
+
+          Lucid.h2_ "Task Status"
+          Lucid.div_ [Lucid.class_ "stats-grid"] <| do
+            statCard "Open" (TaskCore.openTasks stats) "badge-open"
+            statCard "In Progress" (TaskCore.inProgressTasks stats) "badge-inprogress"
+            statCard "Review" (TaskCore.reviewTasks stats) "badge-review"
+            statCard "Approved" (TaskCore.approvedTasks stats) "badge-approved"
+            statCard "Done" (TaskCore.doneTasks stats) "badge-done"
+
+          Lucid.h2_ <| do
+            "Ready Queue "
+            Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "ready-link"]
+              <| Lucid.toHtml ("(" <> tshow (length readyTasks) <> " tasks)")
+          if null readyTasks
+            then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks ready for work."
+            else
+              Lucid.div_ [Lucid.class_ "task-list"]
+                <| traverse_ renderTaskCard (take 5 readyTasks)
+
+          Lucid.h2_ "Recent Activity"
+          if null recentTasks
+            then Lucid.p_ [Lucid.class_ "empty-msg"] "No recent tasks."
+            else
+              Lucid.div_ [Lucid.class_ "task-list"]
+                <| traverse_ renderTaskCard recentTasks
     where
       statCard :: (Monad m) => Text -> Int -> Text -> Lucid.HtmlT m ()
       statCard label count badgeClass =
@@ -136,202 +177,69 @@ instance Lucid.ToHtml HomePage where
           Lucid.div_ [Lucid.class_ "stat-count"] (Lucid.toHtml (tshow count))
           Lucid.div_ [Lucid.class_ "stat-label"] (Lucid.toHtml label)
 
-      renderTaskCard :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
-      renderTaskCard t =
-        Lucid.div_ [Lucid.class_ "task-card"] <| do
-          Lucid.div_ [Lucid.class_ "task-header"] <| do
-            Lucid.a_
-              [ Lucid.class_ "task-id",
-                Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
-              ]
-              (Lucid.toHtml (TaskCore.taskId t))
-            statusBadge (TaskCore.taskStatus t)
-            Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
-          Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
-      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
-      statusBadge status =
-        let (cls, label) = case status of
-              TaskCore.Open -> ("badge badge-open", "Open")
-              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
-              TaskCore.Review -> ("badge badge-review", "Review")
-              TaskCore.Approved -> ("badge badge-approved", "Approved")
-              TaskCore.Done -> ("badge badge-done", "Done")
-         in Lucid.span_ [Lucid.class_ cls] label
-
-homeStyles :: Text
-homeStyles =
-  "* { box-sizing: border-box; } \
-  \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
-  \       margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
-  \h1 { margin: 0 0 16px 0; } \
-  \h2 { margin: 24px 0 12px 0; color: #374151; font-size: 18px; } \
-  \.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; } \
-  \.action-btn { display: inline-block; padding: 10px 16px; background: white; \
-  \              border: 1px solid #d1d5db; border-radius: 6px; color: #374151; \
-  \              text-decoration: none; font-size: 14px; font-weight: 500; } \
-  \.action-btn:hover { background: #f9fafb; } \
-  \.action-btn-primary { background: #0066cc; color: white; border-color: #0066cc; } \
-  \.action-btn-primary:hover { background: #0052a3; } \
-  \.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 12px; } \
-  \.stat-card { background: white; border-radius: 8px; padding: 16px; text-align: center; \
-  \             box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-  \.stat-count { font-size: 28px; font-weight: 700; } \
-  \.stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; } \
-  \.stat-card.badge-open { border-left: 4px solid #f59e0b; } \
-  \.stat-card.badge-open .stat-count { color: #92400e; } \
-  \.stat-card.badge-inprogress { border-left: 4px solid #3b82f6; } \
-  \.stat-card.badge-inprogress .stat-count { color: #1e40af; } \
-  \.stat-card.badge-review { border-left: 4px solid #8b5cf6; } \
-  \.stat-card.badge-review .stat-count { color: #6b21a8; } \
-  \.stat-card.badge-approved { border-left: 4px solid #10b981; } \
-  \.stat-card.badge-approved .stat-count { color: #065f46; } \
-  \.stat-card.badge-done { border-left: 4px solid #10b981; } \
-  \.stat-card.badge-done .stat-count { color: #065f46; } \
-  \.ready-link { font-size: 14px; color: #0066cc; text-decoration: none; } \
-  \.ready-link:hover { text-decoration: underline; } \
-  \.empty-msg { color: #6b7280; font-style: italic; } \
-  \.task-list { display: flex; flex-direction: column; gap: 8px; } \
-  \.task-card { background: white; border-radius: 8px; padding: 16px; \
-  \             box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-  \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
-  \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
-  \           font-size: 14px; padding: 4px 0; } \
-  \.task-id:hover { text-decoration: underline; } \
-  \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
-  \         font-size: 12px; font-weight: 500; } \
-  \.badge-open { background: #fef3c7; color: #92400e; } \
-  \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
-  \.badge-review { background: #ede9fe; color: #6b21a8; } \
-  \.badge-approved { background: #d1fae5; color: #065f46; } \
-  \.badge-done { background: #d1fae5; color: #065f46; } \
-  \.priority { font-size: 12px; color: #6b7280; } \
-  \.task-title { font-size: 16px; margin: 0; }"
-
 instance Lucid.ToHtml ReadyQueuePage where
   toHtmlRaw = Lucid.toHtml
   toHtml (ReadyQueuePage tasks) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ "Ready Queue - Jr Web UI"
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ readyStyles
+      pageHead "Ready Queue - Jr"
       Lucid.body_ <| do
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
-        Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
-        if null tasks
-          then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
-          else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTask tasks
-    where
-      readyStyles :: Text
-      readyStyles =
-        "* { box-sizing: border-box; } \
-        \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
-        \       margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
-        \h1 { margin: 16px 0; } \
-        \.count-badge { background: #0066cc; color: white; padding: 4px 10px; \
-        \               border-radius: 12px; font-size: 14px; vertical-align: middle; } \
-        \.empty-msg { color: #6b7280; font-style: italic; } \
-        \.task-list { display: flex; flex-direction: column; gap: 8px; } \
-        \.task-card { background: white; border-radius: 8px; padding: 16px; \
-        \             box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-        \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
-        \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
-        \           font-size: 14px; padding: 4px 0; } \
-        \.task-id:hover { text-decoration: underline; } \
-        \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
-        \         font-size: 12px; font-weight: 500; } \
-        \.badge-open { background: #fef3c7; color: #92400e; } \
-        \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
-        \.badge-review { background: #ede9fe; color: #6b21a8; } \
-        \.badge-approved { background: #d1fae5; color: #065f46; } \
-        \.badge-done { background: #d1fae5; color: #065f46; } \
-        \.priority { font-size: 12px; color: #6b7280; } \
-        \.task-title { font-size: 16px; margin: 0; }"
-
-      renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
-      renderTask t =
-        Lucid.div_ [Lucid.class_ "task-card"] <| do
-          Lucid.div_ [Lucid.class_ "task-header"] <| do
-            Lucid.a_
-              [ Lucid.class_ "task-id",
-                Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
-              ]
-              (Lucid.toHtml (TaskCore.taskId t))
-            statusBadge (TaskCore.taskStatus t)
-            Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
-          Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
-      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
-      statusBadge status =
-        let (cls, label) = case status of
-              TaskCore.Open -> ("badge badge-open", "Open")
-              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
-              TaskCore.Review -> ("badge badge-review", "Review")
-              TaskCore.Approved -> ("badge badge-approved", "Approved")
-              TaskCore.Done -> ("badge badge-done", "Done")
-         in Lucid.span_ [Lucid.class_ cls] label
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
+          Lucid.h1_ <| Lucid.toHtml ("Ready Queue (" <> tshow (length tasks) <> " tasks)")
+          if null tasks
+            then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks are ready for work."
+            else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
 
 instance Lucid.ToHtml TaskListPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (TaskListPage tasks filters) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ "Tasks - Jr Web UI"
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ styles
+      pageHead "Tasks - Jr"
       Lucid.body_ <| do
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
-        Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
-
-        Lucid.div_ [Lucid.class_ "filter-form"] <| do
-          Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/tasks"] <| do
-            Lucid.div_ [Lucid.class_ "filter-row"] <| do
-              Lucid.div_ [Lucid.class_ "filter-group"] <| do
-                Lucid.label_ [Lucid.for_ "status"] "Status:"
-                Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
-                  Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
-                  statusFilterOption TaskCore.Open (filterStatus filters)
-                  statusFilterOption TaskCore.InProgress (filterStatus filters)
-                  statusFilterOption TaskCore.Review (filterStatus filters)
-                  statusFilterOption TaskCore.Approved (filterStatus filters)
-                  statusFilterOption TaskCore.Done (filterStatus filters)
-
-              Lucid.div_ [Lucid.class_ "filter-group"] <| do
-                Lucid.label_ [Lucid.for_ "priority"] "Priority:"
-                Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
-                  Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
-                  priorityFilterOption TaskCore.P0 (filterPriority filters)
-                  priorityFilterOption TaskCore.P1 (filterPriority filters)
-                  priorityFilterOption TaskCore.P2 (filterPriority filters)
-                  priorityFilterOption TaskCore.P3 (filterPriority filters)
-                  priorityFilterOption TaskCore.P4 (filterPriority filters)
-
-              Lucid.div_ [Lucid.class_ "filter-group"] <| do
-                Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
-                Lucid.input_
-                  [ Lucid.type_ "text",
-                    Lucid.name_ "namespace",
-                    Lucid.id_ "namespace",
-                    Lucid.class_ "filter-input",
-                    Lucid.placeholder_ "e.g. Omni/Jr",
-                    Lucid.value_ (fromMaybe "" (filterNamespace filters))
-                  ]
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/"] "← Back to Dashboard"
+          Lucid.h1_ <| Lucid.toHtml ("Tasks (" <> tshow (length tasks) <> ")")
+
+          Lucid.div_ [Lucid.class_ "filter-form"] <| do
+            Lucid.form_ [Lucid.method_ "GET", Lucid.action_ "/tasks"] <| do
+              Lucid.div_ [Lucid.class_ "filter-row"] <| do
+                Lucid.div_ [Lucid.class_ "filter-group"] <| do
+                  Lucid.label_ [Lucid.for_ "status"] "Status:"
+                  Lucid.select_ [Lucid.name_ "status", Lucid.id_ "status", Lucid.class_ "filter-select"] <| do
+                    Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterStatus filters)) "All"
+                    statusFilterOption TaskCore.Open (filterStatus filters)
+                    statusFilterOption TaskCore.InProgress (filterStatus filters)
+                    statusFilterOption TaskCore.Review (filterStatus filters)
+                    statusFilterOption TaskCore.Approved (filterStatus filters)
+                    statusFilterOption TaskCore.Done (filterStatus filters)
+
+                Lucid.div_ [Lucid.class_ "filter-group"] <| do
+                  Lucid.label_ [Lucid.for_ "priority"] "Priority:"
+                  Lucid.select_ [Lucid.name_ "priority", Lucid.id_ "priority", Lucid.class_ "filter-select"] <| do
+                    Lucid.option_ ([Lucid.value_ ""] <> maybeSelected Nothing (filterPriority filters)) "All"
+                    priorityFilterOption TaskCore.P0 (filterPriority filters)
+                    priorityFilterOption TaskCore.P1 (filterPriority filters)
+                    priorityFilterOption TaskCore.P2 (filterPriority filters)
+                    priorityFilterOption TaskCore.P3 (filterPriority filters)
+                    priorityFilterOption TaskCore.P4 (filterPriority filters)
+
+                Lucid.div_ [Lucid.class_ "filter-group"] <| do
+                  Lucid.label_ [Lucid.for_ "namespace"] "Namespace:"
+                  Lucid.input_
+                    [ Lucid.type_ "text",
+                      Lucid.name_ "namespace",
+                      Lucid.id_ "namespace",
+                      Lucid.class_ "filter-input",
+                      Lucid.placeholder_ "e.g. Omni/Jr",
+                      Lucid.value_ (fromMaybe "" (filterNamespace filters))
+                    ]
 
-              Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
-              Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "clear-btn"] "Clear"
+                Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "filter-btn"] "Filter"
+                Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "clear-btn"] "Clear"
 
-        if null tasks
-          then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
-          else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTask tasks
+          if null tasks
+            then Lucid.p_ [Lucid.class_ "empty-msg"] "No tasks match the current filters."
+            else Lucid.div_ [Lucid.class_ "task-list"] <| traverse_ renderTaskCard tasks
     where
       maybeSelected :: (Eq a) => Maybe a -> Maybe a -> [Lucid.Attribute]
       maybeSelected opt current = [Lucid.selected_ "selected" | opt == current]
@@ -346,181 +254,108 @@ instance Lucid.ToHtml TaskListPage where
         let attrs = [Lucid.value_ (tshow p)] <> [Lucid.selected_ "selected" | Just p == current]
          in Lucid.option_ attrs (Lucid.toHtml (tshow p))
 
-      styles :: Text
-      styles =
-        "* { box-sizing: border-box; } \
-        \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
-        \       margin: 0; padding: 16px; background: #f5f5f5; max-width: 900px; } \
-        \h1 { margin: 16px 0; } \
-        \.filter-form { background: white; border-radius: 8px; padding: 16px; \
-        \               box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 16px; } \
-        \.filter-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; } \
-        \.filter-group { display: flex; flex-direction: column; gap: 4px; } \
-        \.filter-group label { font-size: 12px; color: #6b7280; font-weight: 500; } \
-        \.filter-select, .filter-input { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; \
-        \                                font-size: 14px; min-width: 120px; } \
-        \.filter-input { min-width: 150px; } \
-        \.filter-btn { padding: 8px 16px; background: #0066cc; color: white; border: none; \
-        \              border-radius: 4px; font-size: 14px; cursor: pointer; } \
-        \.filter-btn:hover { background: #0052a3; } \
-        \.clear-btn { padding: 8px 16px; background: #6b7280; color: white; border: none; \
-        \             border-radius: 4px; font-size: 14px; cursor: pointer; text-decoration: none; } \
-        \.clear-btn:hover { background: #4b5563; } \
-        \.empty-msg { color: #6b7280; font-style: italic; } \
-        \.task-list { display: flex; flex-direction: column; gap: 8px; } \
-        \.task-card { background: white; border-radius: 8px; padding: 16px; \
-        \             box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-        \.task-header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px; } \
-        \.task-id { font-family: monospace; color: #0066cc; text-decoration: none; \
-        \           font-size: 14px; padding: 4px 0; } \
-        \.task-id:hover { text-decoration: underline; } \
-        \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
-        \         font-size: 12px; font-weight: 500; } \
-        \.badge-open { background: #fef3c7; color: #92400e; } \
-        \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
-        \.badge-review { background: #ede9fe; color: #6b21a8; } \
-        \.badge-approved { background: #d1fae5; color: #065f46; } \
-        \.badge-done { background: #d1fae5; color: #065f46; } \
-        \.priority { font-size: 12px; color: #6b7280; } \
-        \.task-title { font-size: 16px; margin: 0; }"
-
-      renderTask :: (Monad m) => TaskCore.Task -> Lucid.HtmlT m ()
-      renderTask t =
-        Lucid.div_ [Lucid.class_ "task-card"] <| do
-          Lucid.div_ [Lucid.class_ "task-header"] <| do
-            Lucid.a_
-              [ Lucid.class_ "task-id",
-                Lucid.href_ ("/tasks/" <> TaskCore.taskId t)
-              ]
-              (Lucid.toHtml (TaskCore.taskId t))
-            statusBadge (TaskCore.taskStatus t)
-            Lucid.span_ [Lucid.class_ "priority"] (Lucid.toHtml (tshow (TaskCore.taskPriority t)))
-          Lucid.p_ [Lucid.class_ "task-title"] (Lucid.toHtml (TaskCore.taskTitle t))
-
-      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
-      statusBadge status =
-        let (cls, label) = case status of
-              TaskCore.Open -> ("badge badge-open", "Open")
-              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
-              TaskCore.Review -> ("badge badge-review", "Review")
-              TaskCore.Approved -> ("badge badge-approved", "Approved")
-              TaskCore.Done -> ("badge badge-done", "Done")
-         in Lucid.span_ [Lucid.class_ cls] label
-
 instance Lucid.ToHtml TaskDetailPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (TaskDetailNotFound tid) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ "Task Not Found - Jr Web UI"
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ detailStyles
+      pageHead "Task Not Found - Jr"
       Lucid.body_ <| do
-        Lucid.h1_ "Task Not Found"
-        Lucid.p_ <| do
-          "The task "
-          Lucid.code_ (Lucid.toHtml tid)
-          " could not be found."
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.h1_ "Task Not Found"
+          Lucid.p_ <| do
+            "The task "
+            Lucid.code_ (Lucid.toHtml tid)
+            " could not be found."
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
   toHtml (TaskDetailFound task allTasks) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ <| Lucid.toHtml (TaskCore.taskId task <> " - Jr Web UI")
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ detailStyles
+      pageHead (TaskCore.taskId task <> " - Jr")
       Lucid.body_ <| do
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
-
-        Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
-
-        Lucid.div_ [Lucid.class_ "task-detail"] <| do
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
-            Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
-
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Type:"
-            Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
-
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
-            Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
-
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Priority:"
-            Lucid.span_ [Lucid.class_ "detail-value"] <| do
-              Lucid.toHtml (tshow (TaskCore.taskPriority task))
-              Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
-
-          case TaskCore.taskNamespace task of
-            Nothing -> pure ()
-            Just ns ->
-              Lucid.div_ [Lucid.class_ "detail-row"] <| do
-                Lucid.span_ [Lucid.class_ "detail-label"] "Namespace:"
-                Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml ns)
-
-          case TaskCore.taskParent task of
-            Nothing -> pure ()
-            Just pid ->
-              Lucid.div_ [Lucid.class_ "detail-row"] <| do
-                Lucid.span_ [Lucid.class_ "detail-label"] "Parent:"
-                Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "detail-value task-link"] (Lucid.toHtml pid)
-
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Created:"
-            Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskCreatedAt task)))
-
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Updated:"
-            Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskUpdatedAt task)))
-
-          let deps = TaskCore.taskDependencies task
-          unless (null deps) <| do
-            Lucid.div_ [Lucid.class_ "detail-section"] <| do
-              Lucid.h3_ "Dependencies"
-              Lucid.ul_ [Lucid.class_ "dep-list"] <| do
-                traverse_ renderDependency deps
-
-          case TaskCore.taskDescription task of
-            Nothing -> pure ()
-            Just desc ->
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
+
+          Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
+
+          Lucid.div_ [Lucid.class_ "task-detail"] <| do
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
+              Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
+
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Type:"
+              Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskType task)))
+
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
+              Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
+
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Priority:"
+              Lucid.span_ [Lucid.class_ "detail-value"] <| do
+                Lucid.toHtml (tshow (TaskCore.taskPriority task))
+                Lucid.span_ [Lucid.class_ "priority-desc"] (Lucid.toHtml (priorityDesc (TaskCore.taskPriority task)))
+
+            case TaskCore.taskNamespace task of
+              Nothing -> pure ()
+              Just ns ->
+                Lucid.div_ [Lucid.class_ "detail-row"] <| do
+                  Lucid.span_ [Lucid.class_ "detail-label"] "Namespace:"
+                  Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml ns)
+
+            case TaskCore.taskParent task of
+              Nothing -> pure ()
+              Just pid ->
+                Lucid.div_ [Lucid.class_ "detail-row"] <| do
+                  Lucid.span_ [Lucid.class_ "detail-label"] "Parent:"
+                  Lucid.a_ [Lucid.href_ ("/tasks/" <> pid), Lucid.class_ "detail-value task-link"] (Lucid.toHtml pid)
+
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Created:"
+              Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskCreatedAt task)))
+
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Updated:"
+              Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (tshow (TaskCore.taskUpdatedAt task)))
+
+            let deps = TaskCore.taskDependencies task
+            unless (null deps) <| do
+              Lucid.div_ [Lucid.class_ "detail-section"] <| do
+                Lucid.h3_ "Dependencies"
+                Lucid.ul_ [Lucid.class_ "dep-list"] <| do
+                  traverse_ renderDependency deps
+
+            case TaskCore.taskDescription task of
+              Nothing -> pure ()
+              Just desc ->
+                Lucid.div_ [Lucid.class_ "detail-section"] <| do
+                  Lucid.h3_ "Description"
+                  Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml desc)
+
+            let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
+            unless (null children) <| do
               Lucid.div_ [Lucid.class_ "detail-section"] <| do
-                Lucid.h3_ "Description"
-                Lucid.pre_ [Lucid.class_ "description"] (Lucid.toHtml desc)
-
-          let children = filter (maybe False (TaskCore.matchesId (TaskCore.taskId task)) <. TaskCore.taskParent) allTasks
-          unless (null children) <| do
-            Lucid.div_ [Lucid.class_ "detail-section"] <| do
-              Lucid.h3_ "Child Tasks"
-              Lucid.ul_ [Lucid.class_ "child-list"] <| do
-                traverse_ renderChild children
-
-        when (TaskCore.taskStatus task == TaskCore.Review) <| do
-          Lucid.div_ [Lucid.class_ "review-link-section"] <| do
-            Lucid.a_
-              [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
-                Lucid.class_ "review-link-btn"
-              ]
-              "Review This Task"
-
-        Lucid.div_ [Lucid.class_ "status-form"] <| do
-          Lucid.h3_ "Update Status"
-          Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/status")] <| do
-            Lucid.select_ [Lucid.name_ "status", Lucid.class_ "status-select"] <| do
-              statusOption TaskCore.Open (TaskCore.taskStatus task)
-              statusOption TaskCore.InProgress (TaskCore.taskStatus task)
-              statusOption TaskCore.Review (TaskCore.taskStatus task)
-              statusOption TaskCore.Approved (TaskCore.taskStatus task)
-              statusOption TaskCore.Done (TaskCore.taskStatus task)
-            Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Submit"
+                Lucid.h3_ "Child Tasks"
+                Lucid.ul_ [Lucid.class_ "child-list"] <| do
+                  traverse_ renderChild children
+
+          when (TaskCore.taskStatus task == TaskCore.Review) <| do
+            Lucid.div_ [Lucid.class_ "review-link-section"] <| do
+              Lucid.a_
+                [ Lucid.href_ ("/tasks/" <> TaskCore.taskId task <> "/review"),
+                  Lucid.class_ "review-link-btn"
+                ]
+                "Review This Task"
+
+          Lucid.div_ [Lucid.class_ "status-form"] <| do
+            Lucid.h3_ "Update Status"
+            Lucid.form_ [Lucid.method_ "POST", Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/status")] <| do
+              Lucid.select_ [Lucid.name_ "status", Lucid.class_ "status-select"] <| do
+                statusOption TaskCore.Open (TaskCore.taskStatus task)
+                statusOption TaskCore.InProgress (TaskCore.taskStatus task)
+                statusOption TaskCore.Review (TaskCore.taskStatus task)
+                statusOption TaskCore.Approved (TaskCore.taskStatus task)
+                statusOption TaskCore.Done (TaskCore.taskStatus task)
+              Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "submit-btn"] "Submit"
     where
       priorityDesc :: TaskCore.Priority -> Text
       priorityDesc p = case p of
@@ -530,19 +365,9 @@ instance Lucid.ToHtml TaskDetailPage where
         TaskCore.P3 -> " (Low)"
         TaskCore.P4 -> " (Backlog)"
 
-      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
-      statusBadge status =
-        let (cls, label) = case status of
-              TaskCore.Open -> ("badge badge-open", "Open")
-              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
-              TaskCore.Review -> ("badge badge-review", "Review")
-              TaskCore.Approved -> ("badge badge-approved", "Approved")
-              TaskCore.Done -> ("badge badge-done", "Done")
-         in Lucid.span_ [Lucid.class_ cls] label
-
       statusOption :: (Monad m) => TaskCore.Status -> TaskCore.Status -> Lucid.HtmlT m ()
       statusOption opt current =
-        let attrs = if opt == current then [Lucid.value_ (tshow opt), Lucid.selected_ "selected"] else [Lucid.value_ (tshow opt)]
+        let attrs = [Lucid.value_ (tshow opt)] <> [Lucid.selected_ "selected" | opt == current]
          in Lucid.option_ attrs (Lucid.toHtml (tshow opt))
 
       renderDependency :: (Monad m) => TaskCore.Dependency -> Lucid.HtmlT m ()
@@ -558,192 +383,83 @@ instance Lucid.ToHtml TaskDetailPage where
           Lucid.span_ [Lucid.class_ "child-title"] <| Lucid.toHtml (" - " <> TaskCore.taskTitle child)
           Lucid.span_ [Lucid.class_ "child-status"] <| Lucid.toHtml (" [" <> tshow (TaskCore.taskStatus child) <> "]")
 
-detailStyles :: Text
-detailStyles =
-  "* { box-sizing: border-box; } \
-  \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
-  \       margin: 0; padding: 16px; background: #f5f5f5; max-width: 800px; } \
-  \h1 { margin: 16px 0; } \
-  \h3 { margin: 16px 0 8px 0; color: #374151; } \
-  \.task-detail { background: white; border-radius: 8px; padding: 16px; \
-  \               box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-  \.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #e5e7eb; } \
-  \.detail-row:last-child { border-bottom: none; } \
-  \.detail-label { font-weight: 600; width: 120px; color: #6b7280; } \
-  \.detail-value { flex: 1; } \
-  \.detail-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid #e5e7eb; } \
-  \.task-link { color: #0066cc; text-decoration: none; font-family: monospace; } \
-  \.task-link:hover { text-decoration: underline; } \
-  \.dep-list, .child-list { margin: 8px 0; padding-left: 20px; } \
-  \.dep-list li, .child-list li { margin: 4px 0; } \
-  \.dep-type { color: #6b7280; font-size: 14px; } \
-  \.child-title { color: #374151; } \
-  \.child-status { color: #6b7280; font-size: 14px; } \
-  \.description { background: #f9fafb; padding: 12px; border-radius: 4px; \
-  \               font-family: monospace; font-size: 14px; white-space: pre-wrap; margin: 0; } \
-  \.priority-desc { color: #6b7280; margin-left: 4px; } \
-  \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
-  \         font-size: 12px; font-weight: 500; } \
-  \.badge-open { background: #fef3c7; color: #92400e; } \
-  \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
-  \.badge-review { background: #ede9fe; color: #6b21a8; } \
-  \.badge-approved { background: #d1fae5; color: #065f46; } \
-  \.badge-done { background: #d1fae5; color: #065f46; } \
-  \.status-form { margin-top: 24px; background: white; border-radius: 8px; padding: 16px; \
-  \               box-shadow: 0 1px 3px rgba(0,0,0,0.1); } \
-  \.status-select { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; \
-  \                 font-size: 14px; margin-right: 8px; } \
-  \.submit-btn { padding: 8px 16px; background: #0066cc; color: white; border: none; \
-  \              border-radius: 4px; font-size: 14px; cursor: pointer; } \
-  \.submit-btn:hover { background: #0052a3; } \
-  \.review-link-section { margin: 16px 0; } \
-  \.review-link-btn { display: inline-block; padding: 12px 24px; background: #8b5cf6; \
-  \                   color: white; text-decoration: none; border-radius: 6px; \
-  \                   font-size: 16px; font-weight: 500; } \
-  \.review-link-btn:hover { background: #7c3aed; }"
-
 instance Lucid.ToHtml TaskReviewPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (ReviewPageNotFound tid) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ "Task Not Found - Jr Review"
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ reviewStyles
+      pageHead "Task Not Found - Jr Review"
       Lucid.body_ <| do
-        Lucid.h1_ "Task Not Found"
-        Lucid.p_ <| do
-          "The task "
-          Lucid.code_ (Lucid.toHtml tid)
-          " could not be found."
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ "/tasks"] "<- Back to Tasks"
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.h1_ "Task Not Found"
+          Lucid.p_ <| do
+            "The task "
+            Lucid.code_ (Lucid.toHtml tid)
+            " could not be found."
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ "/tasks"] "← Back to Tasks"
   toHtml (ReviewPageFound task reviewInfo) =
     Lucid.doctypehtml_ <| do
-      Lucid.head_ <| do
-        Lucid.title_ <| Lucid.toHtml ("Review: " <> TaskCore.taskId task <> " - Jr")
-        Lucid.meta_ [Lucid.charset_ "utf-8"]
-        Lucid.meta_
-          [ Lucid.name_ "viewport",
-            Lucid.content_ "width=device-width, initial-scale=1"
-          ]
-        Lucid.style_ reviewStyles
+      pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
       Lucid.body_ <| do
-        Lucid.p_ <| Lucid.a_ [Lucid.href_ ("/tasks/" <> TaskCore.taskId task)] "<- Back to Task"
-
-        Lucid.h1_ "Review Task"
-
-        Lucid.div_ [Lucid.class_ "task-summary"] <| do
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
-            Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
-            Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
-          Lucid.div_ [Lucid.class_ "detail-row"] <| do
-            Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
-            Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
-
-        case reviewInfo of
-          ReviewNoCommit -> do
-            Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
-              Lucid.h3_ "No Commit Found"
-              Lucid.p_ "No commit with this task ID was found in the git history."
-              Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
-          ReviewMergeConflict commitSha conflictFiles -> do
-            Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
-              Lucid.h3_ "Merge Conflict Detected"
-              Lucid.p_ <| do
-                "Commit "
-                Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
-                " cannot be cleanly merged."
-              Lucid.p_ "Conflicting files:"
-              Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
-          ReviewReady commitSha diffText -> do
-            Lucid.div_ [Lucid.class_ "diff-section"] <| do
-              Lucid.h3_ <| do
-                "Commit: "
-                Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
-              Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
-
-            Lucid.div_ [Lucid.class_ "review-actions"] <| do
-              Lucid.form_
-                [ Lucid.method_ "POST",
-                  Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
-                  Lucid.class_ "inline-form"
-                ]
-                <| do
-                  Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
+        Lucid.div_ [Lucid.class_ "container"] <| do
+          Lucid.p_ [Lucid.class_ "back-link"] <| Lucid.a_ [Lucid.href_ ("/tasks/" <> TaskCore.taskId task)] "← Back to Task"
+
+          Lucid.h1_ "Review Task"
+
+          Lucid.div_ [Lucid.class_ "task-summary"] <| do
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "ID:"
+              Lucid.code_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskId task))
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Title:"
+              Lucid.span_ [Lucid.class_ "detail-value"] (Lucid.toHtml (TaskCore.taskTitle task))
+            Lucid.div_ [Lucid.class_ "detail-row"] <| do
+              Lucid.span_ [Lucid.class_ "detail-label"] "Status:"
+              Lucid.span_ [Lucid.class_ "detail-value"] <| statusBadge (TaskCore.taskStatus task)
+
+          case reviewInfo of
+            ReviewNoCommit ->
+              Lucid.div_ [Lucid.class_ "no-commit-msg"] <| do
+                Lucid.h3_ "No Commit Found"
+                Lucid.p_ "No commit with this task ID was found in the git history."
+                Lucid.p_ "The worker may not have completed yet, or the commit message doesn't include the task ID."
+            ReviewMergeConflict commitSha conflictFiles ->
+              Lucid.div_ [Lucid.class_ "conflict-warning"] <| do
+                Lucid.h3_ "Merge Conflict Detected"
+                Lucid.p_ <| do
+                  "Commit "
+                  Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+                  " cannot be cleanly merged."
+                Lucid.p_ "Conflicting files:"
+                Lucid.ul_ <| traverse_ (Lucid.li_ <. Lucid.toHtml) conflictFiles
+            ReviewReady commitSha diffText -> do
+              Lucid.div_ [Lucid.class_ "diff-section"] <| do
+                Lucid.h3_ <| do
+                  "Commit: "
+                  Lucid.code_ (Lucid.toHtml (Text.take 8 commitSha))
+                Lucid.pre_ [Lucid.class_ "diff-block"] (Lucid.toHtml diffText)
+
+              Lucid.div_ [Lucid.class_ "review-actions"] <| do
+                Lucid.form_
+                  [ Lucid.method_ "POST",
+                    Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/accept"),
+                    Lucid.class_ "inline-form"
+                  ]
+                  <| do
+                    Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "accept-btn"] "Accept"
 
-              Lucid.form_
-                [ Lucid.method_ "POST",
-                  Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
-                  Lucid.class_ "reject-form"
-                ]
-                <| do
-                  Lucid.textarea_
-                    [ Lucid.name_ "notes",
-                      Lucid.class_ "reject-notes",
-                      Lucid.placeholder_ "Rejection notes (optional)"
-                    ]
-                    ""
-                  Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
-    where
-      statusBadge :: (Monad m) => TaskCore.Status -> Lucid.HtmlT m ()
-      statusBadge status =
-        let (cls, label) = case status of
-              TaskCore.Open -> ("badge badge-open", "Open")
-              TaskCore.InProgress -> ("badge badge-inprogress", "In Progress")
-              TaskCore.Review -> ("badge badge-review", "Review")
-              TaskCore.Approved -> ("badge badge-approved", "Approved")
-              TaskCore.Done -> ("badge badge-done", "Done")
-         in Lucid.span_ [Lucid.class_ cls] label
-
-reviewStyles :: Text
-reviewStyles =
-  "* { box-sizing: border-box; } \
-  \body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \
-  \       margin: 0; padding: 16px; background: #f5f5f5; max-width: 1000px; } \
-  \h1 { margin: 16px 0; } \
-  \h3 { margin: 16px 0 8px 0; color: #374151; } \
-  \.task-summary { background: white; border-radius: 8px; padding: 16px; \
-  \                box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 16px; } \
-  \.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #e5e7eb; } \
-  \.detail-row:last-child { border-bottom: none; } \
-  \.detail-label { font-weight: 600; width: 100px; color: #6b7280; } \
-  \.detail-value { flex: 1; } \
-  \.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; \
-  \         font-size: 12px; font-weight: 500; } \
-  \.badge-open { background: #fef3c7; color: #92400e; } \
-  \.badge-inprogress { background: #dbeafe; color: #1e40af; } \
-  \.badge-review { background: #ede9fe; color: #6b21a8; } \
-  \.badge-approved { background: #d1fae5; color: #065f46; } \
-  \.badge-done { background: #d1fae5; color: #065f46; } \
-  \.no-commit-msg { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; \
-  \                 padding: 16px; margin: 16px 0; } \
-  \.conflict-warning { background: #f8d7da; border: 1px solid #dc3545; border-radius: 8px; \
-  \                    padding: 16px; margin: 16px 0; } \
-  \.diff-section { background: white; border-radius: 8px; padding: 16px; \
-  \                box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin: 16px 0; } \
-  \.diff-block { background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 4px; \
-  \              font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 13px; \
-  \              overflow-x: auto; white-space: pre; margin: 0; max-height: 600px; overflow-y: auto; } \
-  \.review-actions { background: white; border-radius: 8px; padding: 16px; \
-  \                  box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; gap: 16px; \
-  \                  align-items: flex-start; flex-wrap: wrap; } \
-  \.inline-form { display: inline-block; } \
-  \.reject-form { display: flex; gap: 8px; flex: 1; min-width: 300px; } \
-  \.reject-notes { flex: 1; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; \
-  \                font-size: 14px; resize: vertical; min-height: 38px; } \
-  \.accept-btn { padding: 10px 24px; background: #10b981; color: white; border: none; \
-  \              border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; } \
-  \.accept-btn:hover { background: #059669; } \
-  \.reject-btn { padding: 10px 24px; background: #ef4444; color: white; border: none; \
-  \              border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; } \
-  \.reject-btn:hover { background: #dc2626; }"
+                Lucid.form_
+                  [ Lucid.method_ "POST",
+                    Lucid.action_ ("/tasks/" <> TaskCore.taskId task <> "/reject"),
+                    Lucid.class_ "reject-form"
+                  ]
+                  <| do
+                    Lucid.textarea_
+                      [ Lucid.name_ "notes",
+                        Lucid.class_ "reject-notes",
+                        Lucid.placeholder_ "Rejection notes (optional)"
+                      ]
+                      ""
+                    Lucid.button_ [Lucid.type_ "submit", Lucid.class_ "reject-btn"] "Reject"
 
 api :: Proxy API
 api = Proxy
@@ -751,6 +467,7 @@ api = Proxy
 server :: Server API
 server =
   homeHandler
+    :<|> styleHandler
     :<|> readyQueueHandler
     :<|> taskListHandler
     :<|> taskDetailHandler
@@ -759,6 +476,9 @@ server =
     :<|> taskAcceptHandler
     :<|> taskRejectHandler
   where
+    styleHandler :: Servant.Handler LazyText.Text
+    styleHandler = pure Style.css
+
     homeHandler :: Servant.Handler HomePage
     homeHandler = do
       stats <- liftIO <| TaskCore.getTaskStats Nothing
diff --git a/Omni/Jr/Web/Style.hs b/Omni/Jr/Web/Style.hs
new file mode 100644
index 00000000..e2377b53
--- /dev/null
+++ b/Omni/Jr/Web/Style.hs
@@ -0,0 +1,510 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- : dep clay
+module Omni.Jr.Web.Style
+  ( css,
+    statusBadgeClass,
+    priorityBadgeClass,
+  )
+where
+
+import Alpha hiding (wrap, (**), (|>))
+import Clay
+import qualified Clay.Flexbox as Flexbox
+import qualified Clay.Media as Media
+import qualified Clay.Stylesheet as Stylesheet
+import qualified Data.List.NonEmpty as NE
+import qualified Data.Text.Lazy as LazyText
+
+css :: LazyText.Text
+css = render stylesheet
+
+stylesheet :: Css
+stylesheet = do
+  baseStyles
+  layoutStyles
+  navigationStyles
+  cardStyles
+  statusBadges
+  buttonStyles
+  formStyles
+  responsiveStyles
+  darkModeStyles
+
+baseStyles :: Css
+baseStyles = do
+  star ? boxSizing borderBox
+  html <> body ? do
+    margin (px 0) (px 0) (px 0) (px 0)
+    padding (px 0) (px 0) (px 0) (px 0)
+  body ? do
+    fontFamily
+      [ "-apple-system",
+        "BlinkMacSystemFont",
+        "Segoe UI",
+        "Roboto",
+        "Helvetica Neue",
+        "Arial",
+        "Noto Sans",
+        "sans-serif"
+      ]
+      [sansSerif]
+    fontSize (px 16)
+    lineHeight (em 1.5)
+    color "#1f2937"
+    backgroundColor "#f3f4f6"
+    minHeight (vh 100)
+  "h1" ? do
+    fontSize (px 24)
+    fontWeight bold
+    margin (px 0) (px 0) (em 0.5) (px 0)
+  "h2" ? do
+    fontSize (px 18)
+    fontWeight (weight 600)
+    color "#374151"
+    margin (em 1.5) (px 0) (em 0.75) (px 0)
+  "h3" ? do
+    fontSize (px 16)
+    fontWeight (weight 600)
+    color "#374151"
+    margin (em 1) (px 0) (em 0.5) (px 0)
+  a ? do
+    color "#0066cc"
+    textDecoration none
+  a # hover ? textDecoration underline
+  code ? do
+    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+    fontSize (em 0.9)
+    backgroundColor "#f3f4f6"
+    padding (px 2) (px 6) (px 2) (px 6)
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+  pre ? do
+    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+    fontSize (px 13)
+    backgroundColor "#1e1e1e"
+    color "#d4d4d4"
+    padding (px 16) (px 16) (px 16) (px 16)
+    borderRadius (px 6) (px 6) (px 6) (px 6)
+    overflow auto
+    whiteSpace preWrap
+    maxHeight (px 500)
+
+layoutStyles :: Css
+layoutStyles = do
+  ".container" ? do
+    width (pct 100)
+    maxWidth (px 900)
+    margin (px 0) auto (px 0) auto
+    padding (px 16) (px 16) (px 16) (px 16)
+  main_ ? do
+    Stylesheet.key "flex" ("1 0 auto" :: Text)
+  ".page-content" ? do
+    padding (px 0) (px 0) (px 0) (px 0)
+  ".stats-grid" ? do
+    display grid
+    Stylesheet.key "grid-template-columns" ("repeat(auto-fit, minmax(100px, 1fr))" :: Text)
+    Stylesheet.key "gap" ("12px" :: Text)
+  ".task-list" ? do
+    display flex
+    flexDirection column
+    Stylesheet.key "gap" ("8px" :: Text)
+  ".detail-row" ? do
+    display flex
+    flexWrap Flexbox.wrap
+    padding (px 10) (px 0) (px 10) (px 0)
+    borderBottom (px 1) solid "#e5e7eb"
+  ".detail-row" # lastChild ? borderBottom (px 0) none transparent
+  ".detail-label" ? do
+    fontWeight (weight 600)
+    width (px 120)
+    color "#6b7280"
+    minWidth (px 100)
+  ".detail-value" ? do
+    Stylesheet.key "flex" ("1" :: Text)
+    minWidth (px 0)
+  ".detail-section" ? do
+    marginTop (em 1)
+    paddingTop (em 1)
+    borderTop (px 1) solid "#e5e7eb"
+  ".dep-list" <> ".child-list" ? do
+    margin (px 8) (px 0) (px 8) (px 0)
+    paddingLeft (px 24)
+  (".dep-list" ** li) <> (".child-list" ** li) ? margin (px 6) (px 0) (px 6) (px 0)
+  ".dep-type" <> ".child-status" ? do
+    color "#6b7280"
+    fontSize (px 13)
+  ".child-title" ? color "#374151"
+  ".priority-desc" ? do
+    color "#6b7280"
+    marginLeft (px 4)
+
+navigationStyles :: Css
+navigationStyles = do
+  header ? do
+    backgroundColor white
+    padding (px 12) (px 16) (px 12) (px 16)
+    boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.08) (shadow (px 0) (px 2))))
+    marginBottom (px 16)
+  ".nav-content" ? do
+    maxWidth (px 900)
+    margin (px 0) auto (px 0) auto
+    display flex
+    alignItems center
+    justifyContent spaceBetween
+    flexWrap Flexbox.wrap
+    Stylesheet.key "gap" ("12px" :: Text)
+  ".nav-brand" ? do
+    fontSize (px 20)
+    fontWeight bold
+    color "#1f2937"
+    textDecoration none
+  ".nav-brand" # hover ? textDecoration none
+  ".nav-links" ? do
+    display flex
+    Stylesheet.key "gap" ("8px" :: Text)
+    flexWrap Flexbox.wrap
+  ".back-link" ? do
+    display inlineBlock
+    marginBottom (em 0.75)
+    fontSize (px 14)
+  ".actions" ? do
+    display flex
+    flexWrap Flexbox.wrap
+    Stylesheet.key "gap" ("8px" :: Text)
+    marginBottom (px 16)
+
+cardStyles :: Css
+cardStyles = do
+  ".card"
+    <> ".task-card"
+    <> ".stat-card"
+    <> ".task-detail"
+    <> ".task-summary"
+    <> ".filter-form"
+    <> ".status-form"
+    <> ".diff-section"
+    <> ".review-actions"
+    ? do
+      backgroundColor white
+      borderRadius (px 8) (px 8) (px 8) (px 8)
+      padding (px 16) (px 16) (px 16) (px 16)
+      boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.1) (shadow (px 0) (px 1))))
+  ".stat-card" ? textAlign center
+  ".stat-count" ? do
+    fontSize (px 28)
+    fontWeight bold
+  ".stat-label" ? do
+    fontSize (px 12)
+    color "#6b7280"
+    marginTop (px 4)
+  ".stat-card.badge-open" ? do
+    borderLeft (px 4) solid "#f59e0b"
+  (".stat-card.badge-open" |> ".stat-count") ? color "#92400e"
+  ".stat-card.badge-inprogress" ? borderLeft (px 4) solid "#3b82f6"
+  (".stat-card.badge-inprogress" |> ".stat-count") ? color "#1e40af"
+  ".stat-card.badge-review" ? borderLeft (px 4) solid "#8b5cf6"
+  (".stat-card.badge-review" |> ".stat-count") ? color "#6b21a8"
+  ".stat-card.badge-approved" ? borderLeft (px 4) solid "#06b6d4"
+  (".stat-card.badge-approved" |> ".stat-count") ? color "#0e7490"
+  ".stat-card.badge-done" ? borderLeft (px 4) solid "#10b981"
+  (".stat-card.badge-done" |> ".stat-count") ? color "#065f46"
+  ".task-card" ? do
+    transition "box-shadow" (ms 150) ease (sec 0)
+  ".task-card" # hover ? do
+    boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.15) (shadow (px 0) (px 4))))
+  ".task-header" ? do
+    display flex
+    flexWrap Flexbox.wrap
+    alignItems center
+    Stylesheet.key "gap" ("8px" :: Text)
+    marginBottom (px 8)
+  ".task-id" ? do
+    fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
+    color "#0066cc"
+    textDecoration none
+    fontSize (px 14)
+    padding (px 4) (px 0) (px 4) (px 0)
+  ".task-id" # hover ? textDecoration underline
+  ".priority" ? do
+    fontSize (px 12)
+    color "#6b7280"
+  ".task-title" ? do
+    fontSize (px 16)
+    margin (px 0) (px 0) (px 0) (px 0)
+  ".empty-msg" ? do
+    color "#6b7280"
+    fontStyle italic
+  ".ready-link" ? do
+    fontSize (px 14)
+    color "#0066cc"
+  ".count-badge" ? do
+    backgroundColor "#0066cc"
+    color white
+    padding (px 4) (px 10) (px 4) (px 10)
+    borderRadius (px 12) (px 12) (px 12) (px 12)
+    fontSize (px 14)
+    verticalAlign middle
+  ".description" ? do
+    backgroundColor "#f9fafb"
+    padding (px 12) (px 12) (px 12) (px 12)
+    borderRadius (px 4) (px 4) (px 4) (px 4)
+    margin (px 0) (px 0) (px 0) (px 0)
+  ".diff-block" ? do
+    maxHeight (px 600)
+    overflowY auto
+  ".no-commit-msg" ? do
+    backgroundColor "#fff3cd"
+    border (px 1) solid "#ffc107"
+    borderRadius (px 8) (px 8) (px 8) (px 8)
+    padding (px 16) (px 16) (px 16) (px 16)
+    margin (px 16) (px 0) (px 16) (px 0)
+  ".conflict-warning" ? do
+    backgroundColor "#fee2e2"
+    border (px 1) solid "#ef4444"
+    borderRadius (px 8) (px 8) (px 8) (px 8)
+    padding (px 16) (px 16) (px 16) (px 16)
+    margin (px 16) (px 0) (px 16) (px 0)
+
+statusBadges :: Css
+statusBadges = do
+  ".badge" ? do
+    display inlineBlock
+    padding (px 4) (px 10) (px 4) (px 10)
+    borderRadius (px 20) (px 20) (px 20) (px 20)
+    fontSize (px 12)
+    fontWeight (weight 500)
+    whiteSpace nowrap
+  ".badge-open" ? do
+    backgroundColor "#fef3c7"
+    color "#92400e"
+  ".badge-inprogress" ? do
+    backgroundColor "#dbeafe"
+    color "#1e40af"
+  ".badge-review" ? do
+    backgroundColor "#ede9fe"
+    color "#6b21a8"
+  ".badge-approved" ? do
+    backgroundColor "#cffafe"
+    color "#0e7490"
+  ".badge-done" ? do
+    backgroundColor "#d1fae5"
+    color "#065f46"
+
+buttonStyles :: Css
+buttonStyles = do
+  ".btn"
+    <> ".action-btn"
+    <> ".filter-btn"
+    <> ".submit-btn"
+    <> ".accept-btn"
+    <> ".reject-btn"
+    <> ".review-link-btn"
+    ? do
+      display inlineBlock
+      minHeight (px 44)
+      padding (px 10) (px 20) (px 10) (px 20)
+      borderRadius (px 6) (px 6) (px 6) (px 6)
+      border (px 0) none transparent
+      fontSize (px 14)
+      fontWeight (weight 500)
+      textDecoration none
+      cursor pointer
+      textAlign center
+      transition "all" (ms 150) ease (sec 0)
+      Stylesheet.key "touch-action" ("manipulation" :: Text)
+  ".action-btn" ? do
+    backgroundColor white
+    border (px 1) solid "#d1d5db"
+    color "#374151"
+  ".action-btn" # hover ? do
+    backgroundColor "#f9fafb"
+    borderColor "#9ca3af"
+  ".action-btn-primary" <> ".filter-btn" <> ".submit-btn" ? do
+    backgroundColor "#0066cc"
+    color white
+    borderColor "#0066cc"
+  ".action-btn-primary"
+    # hover
+    <> ".filter-btn"
+    # hover
+    <> ".submit-btn"
+    # hover
+    ? do
+      backgroundColor "#0052a3"
+  ".accept-btn" ? do
+    backgroundColor "#10b981"
+    color white
+  ".accept-btn" # hover ? backgroundColor "#059669"
+  ".reject-btn" ? do
+    backgroundColor "#ef4444"
+    color white
+  ".reject-btn" # hover ? backgroundColor "#dc2626"
+  ".clear-btn" ? do
+    display inlineBlock
+    minHeight (px 44)
+    padding (px 10) (px 16) (px 10) (px 16)
+    backgroundColor "#6b7280"
+    color white
+    borderRadius (px 6) (px 6) (px 6) (px 6)
+    textDecoration none
+    fontSize (px 14)
+    cursor pointer
+  ".clear-btn" # hover ? backgroundColor "#4b5563"
+  ".review-link-btn" ? do
+    backgroundColor "#8b5cf6"
+    color white
+  ".review-link-btn" # hover ? backgroundColor "#7c3aed"
+  ".review-link-section" ? margin (px 16) (px 0) (px 16) (px 0)
+
+formStyles :: Css
+formStyles = do
+  ".filter-row" ? do
+    display flex
+    flexWrap Flexbox.wrap
+    Stylesheet.key "gap" ("12px" :: Text)
+    alignItems flexEnd
+  ".filter-group" ? do
+    display flex
+    flexDirection column
+    Stylesheet.key "gap" ("4px" :: Text)
+  (".filter-group" |> label) ? do
+    fontSize (px 12)
+    color "#6b7280"
+    fontWeight (weight 500)
+  ".filter-select" <> ".filter-input" <> ".status-select" ? do
+    minHeight (px 44)
+    padding (px 10) (px 14) (px 10) (px 14)
+    border (px 1) solid "#d1d5db"
+    borderRadius (px 6) (px 6) (px 6) (px 6)
+    fontSize (px 14)
+    minWidth (px 120)
+  ".filter-input" ? minWidth (px 150)
+  ".inline-form" ? display inlineBlock
+  ".reject-form" ? do
+    display flex
+    Stylesheet.key "gap" ("8px" :: Text)
+    Stylesheet.key "flex" ("1" :: Text)
+    minWidth (px 250)
+    flexWrap Flexbox.wrap
+  ".reject-notes" ? do
+    Stylesheet.key "flex" ("1" :: Text)
+    minWidth (px 200)
+    minHeight (px 44)
+    padding (px 10) (px 14) (px 10) (px 14)
+    border (px 1) solid "#d1d5db"
+    borderRadius (px 6) (px 6) (px 6) (px 6)
+    fontSize (px 14)
+    Stylesheet.key "resize" ("vertical" :: Text)
+
+responsiveStyles :: Css
+responsiveStyles = do
+  query Media.screen [Media.maxWidth (px 600)] <| do
+    body ? fontSize (px 15)
+    ".container" ? padding (px 12) (px 12) (px 12) (px 12)
+    ".nav-content" ? do
+      flexDirection column
+      alignItems flexStart
+    ".stats-grid" ? do
+      Stylesheet.key "grid-template-columns" ("repeat(2, 1fr)" :: Text)
+    ".detail-row" ? do
+      flexDirection column
+      Stylesheet.key "gap" ("4px" :: Text)
+    ".detail-label" ? width auto
+    ".filter-row" ? flexDirection column
+    ".filter-group" ? width (pct 100)
+    ".filter-select" <> ".filter-input" ? width (pct 100)
+    ".review-actions" ? do
+      flexDirection column
+    ".reject-form" ? do
+      width (pct 100)
+      flexDirection column
+    ".reject-notes" ? width (pct 100)
+    ".actions" ? flexDirection column
+    ".action-btn" ? width (pct 100)
+
+darkModeStyles :: Css
+darkModeStyles =
+  query Media.screen [prefersDark] <| do
+    body ? do
+      backgroundColor "#111827"
+      color "#f3f4f6"
+    ".card"
+      <> ".task-card"
+      <> ".stat-card"
+      <> ".task-detail"
+      <> ".task-summary"
+      <> ".filter-form"
+      <> ".status-form"
+      <> ".diff-section"
+      <> ".review-actions"
+      ? do
+        backgroundColor "#1f2937"
+        boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.3) (shadow (px 0) (px 2))))
+    header ? do
+      backgroundColor "#1f2937"
+      boxShadow (NE.singleton (bsColor (rgba 0 0 0 0.3) (shadow (px 0) (px 2))))
+    ".nav-brand" ? color "#f3f4f6"
+    "h2" <> "h3" ? color "#d1d5db"
+    a ? color "#60a5fa"
+    ".detail-row" ? borderBottomColor "#374151"
+    ".detail-label"
+      <> ".priority"
+      <> ".dep-type"
+      <> ".child-status"
+      <> ".empty-msg"
+      <> ".stat-label"
+      <> ".priority-desc"
+      ? color "#9ca3af"
+    ".child-title" ? color "#d1d5db"
+    code ? do
+      backgroundColor "#374151"
+      color "#f3f4f6"
+    ".detail-section" ? borderTopColor "#374151"
+    ".description" ? backgroundColor "#374151"
+    ".badge-open" ? do
+      backgroundColor "#78350f"
+      color "#fcd34d"
+    ".badge-inprogress" ? do
+      backgroundColor "#1e3a8a"
+      color "#93c5fd"
+    ".badge-review" ? do
+      backgroundColor "#4c1d95"
+      color "#c4b5fd"
+    ".badge-approved" ? do
+      backgroundColor "#164e63"
+      color "#67e8f9"
+    ".badge-done" ? do
+      backgroundColor "#064e3b"
+      color "#6ee7b7"
+    ".action-btn" ? do
+      backgroundColor "#374151"
+      borderColor "#4b5563"
+      color "#f3f4f6"
+    ".action-btn" # hover ? backgroundColor "#4b5563"
+    ".filter-select" <> ".filter-input" <> ".status-select" <> ".reject-notes" ? do
+      backgroundColor "#374151"
+      borderColor "#4b5563"
+      color "#f3f4f6"
+
+prefersDark :: Stylesheet.Feature
+prefersDark =
+  Stylesheet.Feature "prefers-color-scheme" (Just (Clay.value ("dark" :: Text)))
+
+statusBadgeClass :: Text -> Text
+statusBadgeClass status = case status of
+  "Open" -> "badge badge-open"
+  "InProgress" -> "badge badge-inprogress"
+  "Review" -> "badge badge-review"
+  "Approved" -> "badge badge-approved"
+  "Done" -> "badge badge-done"
+  _ -> "badge"
+
+priorityBadgeClass :: Text -> Text
+priorityBadgeClass priority = case priority of
+  "P0" -> "badge badge-p0"
+  "P1" -> "badge badge-p1"
+  "P2" -> "badge badge-p2"
+  "P3" -> "badge badge-p3"
+  "P4" -> "badge badge-p4"
+  _ -> "badge"