← Back to task

Commit 22d2f1cf

commit 22d2f1cf2f31a9bdeae40efc1718da332500b3bc
Author: Coder Agent <coder@agents.omni>
Date:   Mon Feb 16 21:50:46 2026

    Shared design system: dark-first terminal aesthetic
    
    Create Omni/Web/Style.hs as unified Clay-based design system with:
    - Dark-first color tokens (cBg, cAccent, cFg, etc.)
    - Monospace-forward typography for terminal/hacker aesthetic
    - Reading typography (sans-serif, 16px, 1.7 line-height) for articles
    - iOS webapp meta tags (standalone, safe-area-inset support)
    - Shared nav component and page shell for News/Files
    - Semi-transparent RGBA badges that work on dark backgrounds
    - Mobile responsive breakpoints at 640px
    - Sticky nav with backdrop blur
    
    Migrate all three surfaces to shared tokens:
    - Task Web: replace 244 hardcoded colors with design token refs,
      remove separate darkModeStyles (now redundant)
    - News: use sharedShell + newsreader-specific CSS overlay
    - Files: use sharedShell + files-specific CSS overlay
    
    Also: rename Omni/Web.hs -> Omni/Web/Core.hs to allow Omni/Web/ directory,
    rebrand 'Junior'/'Jr' -> 'omni' throughout nav/breadcrumbs/titles,
    lowercase all nav labels for terminal aesthetic.
    
    Task-Id: t-621

diff --git a/Omni/Ava/Core.hs b/Omni/Ava/Core.hs
index 6c7b8e32..36928e79 100755
--- a/Omni/Ava/Core.hs
+++ b/Omni/Ava/Core.hs
@@ -40,7 +40,7 @@ import qualified Omni.Agent.Memory as Memory
 import qualified Omni.Ava.Telegram.Bot as Telegram
 import qualified Omni.Cli as Cli
 import qualified Omni.Test as Test
-import qualified Omni.Web as Web
+import qualified Omni.Web.Core as Web
 import qualified System.Directory as Dir
 import qualified System.Environment as Environment
 import qualified System.IO as IO
diff --git a/Omni/Newsreader/Web.hs b/Omni/Newsreader/Web.hs
index 1ae74df1..459a1328 100644
--- a/Omni/Newsreader/Web.hs
+++ b/Omni/Newsreader/Web.hs
@@ -42,6 +42,7 @@ import qualified Omni.Newsreader.Cluster as Cluster
 import qualified Omni.Newsreader.Feed as Feed
 import qualified Omni.Newsreader.Ingest as Ingest
 import qualified Omni.Newsreader.Search as Search
+import qualified Omni.Web.Style as WebStyle
 
 -- ============================================================================
 -- Application
@@ -95,10 +96,10 @@ riverPage p' conn req respond = do
   feeds <- Feed.listFeeds conn
   let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
   respond <| htmlResponse HTTP.status200 <| shell "newsreader" <| do
-    nav p'
-    L.div_ [L.class_ "articles"]
+    newsSubNav p'
+    L.div_ [L.class_ "container-narrow"]
       <| if null articles
-        then L.p_ [L.class_ "empty"] "No articles yet. Add some feeds to get started."
+        then L.p_ [L.class_ "empty-state"] "No articles yet. Add some feeds to get started."
         else forM_ articles <| \art -> articleCard p' feedMap art
 
 -- | Topics page: keyword-based clusters.
@@ -109,22 +110,23 @@ topicsPage p' conn respond = do
   feeds <- Feed.listFeeds conn
   let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
   respond <| htmlResponse HTTP.status200 <| shell "topics" <| do
-    nav p'
-    L.h1_ "Topics"
-    if null clusters
-      then L.p_ [L.class_ "empty"] "Not enough articles to form topics yet."
-      else
-        forM_ clusters <| \cluster -> do
-          let arts = Cluster.clusterItems cluster
-              count = length arts
-          L.div_ [L.class_ "topic"] <| do
-            L.h2_ <| do
-              L.toHtml (Cluster.clusterLabel cluster)
-              L.span_ [L.class_ "count"] <| L.toHtml (" (" <> T.pack (show count) <> ")" :: Text)
-            forM_ (take 5 arts) <| \art -> articleCard p' feedMap art
-            when (count > 5)
-              <| L.p_ [L.class_ "muted"]
-              <| L.toHtml ("+ " <> T.pack (show (count - 5)) <> " more" :: Text)
+    newsSubNav p'
+    L.div_ [L.class_ "container-narrow"] <| do
+      L.h1_ "Topics"
+      if null clusters
+        then L.p_ [L.class_ "empty-state"] "Not enough articles to form topics yet."
+        else
+          forM_ clusters <| \cluster -> do
+            let arts = Cluster.clusterItems cluster
+                count = length arts
+            L.div_ [L.class_ "nr-topic"] <| do
+              L.h2_ <| do
+                L.toHtml (Cluster.clusterLabel cluster)
+                L.span_ [L.class_ "count faint"] <| L.toHtml (" (" <> T.pack (show count) <> ")" :: Text)
+              forM_ (take 5 arts) <| \art -> articleCard p' feedMap art
+              when (count > 5)
+                <| L.p_ [L.class_ "muted"]
+                <| L.toHtml ("+ " <> T.pack (show (count - 5)) <> " more" :: Text)
 
 -- | Single article view.
 articlePage :: Pfx -> SQL.Connection -> Text -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
@@ -140,16 +142,16 @@ articlePage p' conn aidText respond = do
           let feedMap = Map.fromList [(fid, f) | f <- feeds, Just fid <- [Feed.feedId f]]
               feedName = maybe "unknown" (fromMaybe "untitled" <. Feed.feedTitle) (Map.lookup (Article.articleFeedId art) feedMap)
           respond <| htmlResponse HTTP.status200 <| shell (Article.articleTitle art) <| do
-            nav p'
-            L.article_ [L.class_ "full-article"] <| do
+            newsSubNav p'
+            L.article_ [L.class_ "nr-article"] <| do
               L.h1_ (L.toHtml (Article.articleTitle art))
               L.div_ [L.class_ "meta"] <| do
-                L.span_ [L.class_ "feed"] (L.toHtml feedName)
+                L.span_ [L.class_ "accent"] (L.toHtml feedName)
                 L.toHtml (" · " :: Text)
-                L.span_ [L.class_ "date"] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
+                L.span_ [] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
                 L.toHtml (" · " :: Text)
                 L.a_ [L.href_ (Article.articleUrl art), L.target_ "_blank"] "original ↗"
-              L.div_ [L.class_ "content"]
+              L.div_ [L.class_ "content reading"]
                 <| L.toHtmlRaw (Article.articleContent art)
 
 -- | Feeds management page.
@@ -157,25 +159,26 @@ feedsPage :: Pfx -> SQL.Connection -> (Wai.Response -> IO Wai.ResponseReceived)
 feedsPage p' conn respond = do
   feeds <- Feed.listFeeds conn
   respond <| htmlResponse HTTP.status200 <| shell "feeds" <| do
-    nav p'
-    L.h1_ "Feeds"
-    L.form_ [L.method_ "POST", L.action_ (p' "/api/feeds"), L.class_ "add-feed"] <| do
-      L.input_ [L.type_ "url", L.name_ "url", L.placeholder_ "https://example.com/feed.xml", L.required_ ""]
-      L.button_ [L.type_ "submit"] "add"
-    if null feeds
-      then L.p_ [L.class_ "empty"] "No feeds yet."
-      else
-        L.ul_ [L.class_ "feed-list"]
-          <| forM_ feeds
-          <| \feed -> do
-            let title = fromMaybe (Feed.feedUrl feed) (Feed.feedTitle feed)
-                fetched = maybe "never" (T.pack <. TimeF.formatTime TimeF.defaultTimeLocale "%Y-%m-%d %H:%M") (Feed.feedLastFetched feed)
-            L.li_ <| do
-              L.div_ (L.toHtml title)
-              L.div_ [L.class_ "muted small"] <| do
-                L.toHtml (Feed.feedUrl feed)
-                L.toHtml (" · last fetched: " :: Text)
-                L.toHtml fetched
+    newsSubNav p'
+    L.div_ [L.class_ "container-narrow"] <| do
+      L.h1_ "Feeds"
+      L.form_ [L.method_ "POST", L.action_ (p' "/api/feeds"), L.class_ "nr-add-feed"] <| do
+        L.input_ [L.type_ "url", L.name_ "url", L.placeholder_ "https://example.com/feed.xml", L.required_ ""]
+        L.button_ [L.type_ "submit", L.class_ "btn btn-primary"] "add"
+      if null feeds
+        then L.p_ [L.class_ "empty-state"] "No feeds yet."
+        else
+          L.ul_ [L.class_ "nr-feed-list"]
+            <| forM_ feeds
+            <| \feed -> do
+              let title = fromMaybe (Feed.feedUrl feed) (Feed.feedTitle feed)
+                  fetched = maybe "never" (T.pack <. TimeF.formatTime TimeF.defaultTimeLocale "%Y-%m-%d %H:%M") (Feed.feedLastFetched feed)
+              L.li_ <| do
+                L.div_ (L.toHtml title)
+                L.div_ [L.class_ "muted text-xs"] <| do
+                  L.toHtml (Feed.feedUrl feed)
+                  L.toHtml (" · last fetched: " :: Text)
+                  L.toHtml fetched
 
 -- | Search page.
 searchPage :: Pfx -> SQL.Connection -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
@@ -188,15 +191,16 @@ searchPage p' conn req respond = do
       then pure []
       else Search.searchArticles conn query 50
   respond <| htmlResponse HTTP.status200 <| shell "search" <| do
-    nav p'
-    L.form_ [L.method_ "GET", L.action_ (p' "/search"), L.class_ "search-form"]
-      <| L.input_ [L.type_ "search", L.name_ "q", L.value_ query, L.placeholder_ "search articles...", L.autofocus_]
-    unless (T.null query)
-      <| if null results
-        then L.p_ [L.class_ "empty"] "No results."
-        else
-          forM_ results <| \sr ->
-            articleCard p' feedMap (Search.srArticle sr)
+    newsSubNav p'
+    L.div_ [L.class_ "container-narrow"] <| do
+      L.form_ [L.method_ "GET", L.action_ (p' "/search"), L.class_ "nr-search-form"]
+        <| L.input_ [L.type_ "search", L.name_ "q", L.value_ query, L.placeholder_ "search articles...", L.autofocus_]
+      unless (T.null query)
+        <| if null results
+          then L.p_ [L.class_ "empty-state"] "No results."
+          else
+            forM_ results <| \sr ->
+              articleCard p' feedMap (Search.srArticle sr)
 
 -- ============================================================================
 -- JSON API
@@ -305,117 +309,94 @@ articleCard p' feedMap art = do
   let feedName = maybe "unknown" (fromMaybe "untitled" <. Feed.feedTitle) (Map.lookup (Article.articleFeedId art) feedMap)
       snippet = T.take 280 (stripTags (Article.articleContent art))
       artUrl = p' ("/article/" <> maybe "" (T.pack <. show <. Article.unArticleId) (Article.articleId art))
-  L.div_ [L.class_ "article-card"] <| do
-    L.a_ [L.href_ artUrl, L.class_ "title"] (L.toHtml (Article.articleTitle art))
-    L.div_ [L.class_ "meta"] <| do
-      L.span_ [L.class_ "feed"] (L.toHtml feedName)
-      L.toHtml (" · " :: Text)
-      L.span_ [L.class_ "date"] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
+  L.div_ [L.class_ "nr-card"] <| do
+    L.a_ [L.href_ artUrl, L.class_ "nr-card-title"] (L.toHtml (Article.articleTitle art))
+    L.div_ [L.class_ "nr-card-meta"] <| do
+      L.span_ [L.class_ "accent"] (L.toHtml feedName)
+      L.span_ [L.class_ "faint"] " · "
+      L.span_ [] (L.toHtml (fmtTime (Article.articlePublishedAt art)))
     unless (T.null snippet)
-      <| L.p_ [L.class_ "snippet"] (L.toHtml snippet)
-
--- | Navigation bar.
-nav :: Pfx -> L.Html ()
-nav p' =
-  L.nav_ <| do
-    L.a_ [L.href_ "/tasks", L.class_ "brand"] "omni"
-    L.span_ [L.class_ "nav-links"] <| do
-      L.a_ [L.href_ "/tasks"] "tasks"
-      L.a_ [L.href_ "/news/"] "news"
-      L.a_ [L.href_ "/files/"] "files"
-      L.a_ [L.href_ (p' "/topics")] "topics"
-      L.a_ [L.href_ (p' "/feeds")] "feeds"
-      L.a_ [L.href_ (p' "/search")] "search"
+      <| L.p_ [L.class_ "nr-card-snippet"] (L.toHtml snippet)
+
+-- | Sub-navigation for newsreader sections.
+newsSubNav :: Pfx -> L.Html ()
+newsSubNav p' =
+  L.div_ [L.class_ "nr-subnav"] <| do
+    L.a_ [L.href_ (p' "/topics"), L.class_ "nr-subnav-link"] "topics"
+    L.a_ [L.href_ (p' "/feeds"), L.class_ "nr-subnav-link"] "feeds"
+    L.a_ [L.href_ (p' "/search"), L.class_ "nr-subnav-link"] "search"
 
 -- | Error page.
 errorPage :: Text -> L.Html ()
 errorPage msg =
   shell msg <| do
-    L.p_ (L.toHtml msg)
+    L.div_ [L.class_ "container"] <| L.p_ [L.class_ "empty-state"] (L.toHtml msg)
 
--- | Page shell with CSS.
+-- | Page shell with shared design system CSS.
 shell :: Text -> L.Html () -> L.Html ()
-shell title body =
-  L.doctypehtml_ <| do
-    L.head_ <| do
-      L.meta_ [L.charset_ "utf-8"]
-      L.meta_ [L.name_ "viewport", L.content_ "width=device-width, initial-scale=1"]
-      L.title_ (L.toHtml title)
-      L.style_ css
-    L.body_ body
+shell title content =
+  WebStyle.sharedShell title "news" <| do
+    L.style_ newsCss
+    content
 
 -- ============================================================================
 -- CSS
 -- ============================================================================
 
-css :: Text
-css =
+-- | Newsreader-specific CSS (layered on top of shared design system).
+newsCss :: Text
+newsCss =
   T.unlines
-    [ ":root { --fg: #1f2937; --bg: #f5f5f5; --muted: #6b7280; --link: #0066cc;",
-      "  --border: #d0d0d0; --hover: #f3f4f6; --card: #ffffff; }",
-      "@media (prefers-color-scheme: dark) {",
-      "  :root { --fg: #d4d4d4; --bg: #111827; --muted: #9ca3af; --link: #60a5fa;",
-      "    --border: #374151; --hover: #1f2937; --card: #111827; }",
-      "}",
-      "* { box-sizing: border-box; margin: 0; padding: 0; }",
-      "body { font: 14px/1.3 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;",
-      "  max-width: 960px; margin: 0 auto; padding: 0.5rem 0.75rem;",
-      "  background: var(--bg); color: var(--fg); min-height: 100vh; }",
-      "a { color: var(--link); text-decoration: none; }",
-      "a:hover { text-decoration: underline; }",
-      "",
-      "nav { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;",
-      "  background: var(--card); border-bottom: 1px solid var(--border);",
-      "  padding: 0.375rem 0.75rem; margin-bottom: 0.5rem; }",
-      ".brand { font-weight: 700; font-size: 1rem; color: var(--link); }",
-      ".brand:hover { text-decoration: none; }",
-      ".nav-links { display: flex; gap: 0.25rem; flex-wrap: wrap; font-size: 0.82rem; }",
-      ".nav-links a { color: var(--muted); padding: 0.25rem 0.6rem; border-radius: 2px; }",
-      ".nav-links a:hover { background: var(--hover); color: var(--fg); text-decoration: none; }",
-      "",
-      ".article-card { margin-bottom: 0.45rem; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; padding: 0.6rem 0.7rem; }",
-      ".article-card .title { font-size: 0.95rem; font-weight: 600; display: block; color: var(--link); }",
-      ".article-card .meta { font-size: 0.78rem; color: var(--muted); margin: 0.15rem 0 0.3rem; }",
-      ".article-card .snippet { font-size: 0.85rem; color: var(--muted); line-height: 1.4; }",
+    [ -- Sub-navigation
+      ".nr-subnav { display: flex; gap: 2px; padding: 4px 16px; border-bottom: 1px solid var(--c-border-subtle); }",
+      ".nr-subnav-link { font-size: 12px; color: var(--c-fg-faint); padding: 4px 10px; border-radius: 4px; }",
+      ".nr-subnav-link:hover { color: var(--c-fg); background: var(--c-bg-hover); text-decoration: none; }",
       "",
-      ".full-article h1 { font-size: 1.25rem; line-height: 1.25; margin-bottom: 0.4rem; }",
-      ".full-article .meta { font-size: 0.82rem; color: var(--muted); margin-bottom: 1rem; }",
-      ".full-article .content p { margin-bottom: 0.85rem; line-height: 1.5; }",
+      -- Article cards
+      ".nr-card { padding: 10px 16px; border-bottom: 1px solid var(--c-border-subtle); transition: background 0.1s; }",
+      ".nr-card:hover { background: var(--c-bg-hover); }",
+      ".nr-card-title { font-size: 14px; font-weight: 600; display: block; color: var(--c-fg); line-height: 1.4; }",
+      ".nr-card-title:hover { color: var(--c-accent); text-decoration: none; }",
+      ".nr-card-meta { font-size: 11px; color: var(--c-fg-muted); margin: 3px 0 4px; }",
+      ".nr-card-snippet { font-size: 13px; color: var(--c-fg-muted); line-height: 1.5; font-family: var(--font-reading); }",
       "",
-      ".topic { margin-bottom: 1rem; }",
-      ".topic h2 { font-size: 1rem; margin-bottom: 0.35rem; }",
-      ".topic .count { font-weight: normal; color: var(--muted); }",
+      -- Full article view
+      ".nr-article { padding: 16px; max-width: 680px; margin: 0 auto; }",
+      ".nr-article h1 { font-size: 1.4rem; line-height: 1.3; margin-bottom: 8px; }",
+      ".nr-article .meta { font-size: 12px; color: var(--c-fg-muted); margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--c-border-subtle); }",
+      ".nr-article .content { font-family: var(--font-reading); font-size: 16px; line-height: 1.7; letter-spacing: -0.011em; }",
+      ".nr-article .content p { margin-bottom: 1em; }",
+      ".nr-article .content img { max-width: 100%; height: auto; border-radius: 6px; }",
+      ".nr-article .content blockquote { border-left: 3px solid var(--c-accent-dim); padding-left: 16px; color: var(--c-fg-muted); margin: 1em 0; }",
+      ".nr-article .content a { text-underline-offset: 3px; }",
       "",
-      ".feed-list { list-style: none; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; }",
-      ".feed-list li { padding: 0.5rem 0.7rem; border-bottom: 1px solid #e5e7eb; }",
-      ".feed-list li:last-child { border-bottom: none; }",
-      ".small { font-size: 0.8rem; }",
-      ".muted { color: var(--muted); }",
-      ".empty { color: var(--muted); font-style: italic; margin: 0.5rem 0; }",
+      -- Topics
+      ".nr-topic { margin-bottom: 20px; }",
+      ".nr-topic h2 { font-size: 1rem; margin-bottom: 6px; }",
+      ".nr-topic .count { font-weight: normal; color: var(--c-fg-faint); }",
       "",
-      ".add-feed { display: flex; gap: 0.5rem; margin-bottom: 0.75rem; }",
-      ".add-feed input { flex: 1; padding: 0.35rem 0.5rem; font-size: 0.85rem;",
-      "  border: 1px solid #d1d5db; background: var(--card); color: var(--fg); border-radius: 2px; }",
-      ".add-feed button { padding: 0.35rem 0.65rem; font-size: 0.82rem;",
-      "  border: 1px solid #d1d5db; background: var(--hover); color: var(--fg);",
-      "  border-radius: 2px; cursor: pointer; }",
+      -- Feed list
+      ".nr-feed-list { list-style: none; }",
+      ".nr-feed-list li { padding: 10px 16px; border-bottom: 1px solid var(--c-border-subtle); }",
+      ".nr-feed-list li:last-child { border-bottom: none; }",
       "",
-      ".search-form { margin-bottom: 0.75rem; }",
-      ".search-form input { width: 100%; padding: 0.45rem 0.5rem; font-size: 0.9rem;",
-      "  border: 1px solid #d1d5db; background: var(--card); color: var(--fg); border-radius: 2px; }",
+      -- Add feed form
+      ".nr-add-feed { display: flex; gap: 8px; margin-bottom: 16px; }",
+      ".nr-add-feed input { flex: 1; }",
+      ".nr-add-feed button { padding: 8px 14px; }",
       "",
-      "@media (max-width: 700px) {",
-      "  body { font-size: 15px; padding: 0.5rem; }",
-      "  nav { padding: 0.5rem; }",
-      "  .nav-links { width: 100%; gap: 0.2rem; }",
-      "  .nav-links a { padding: 0.45rem 0.6rem; font-size: 0.9rem; }",
-      "  .article-card { padding: 0.7rem; }",
-      "  .article-card .title { font-size: 1rem; line-height: 1.3; }",
-      "  .article-card .meta { font-size: 0.82rem; }",
-      "  .article-card .snippet { font-size: 0.9rem; }",
-      "}",
+      -- Search
+      ".nr-search-form { margin-bottom: 16px; }",
+      ".nr-search-form input { width: 100%; }",
       "",
-      "h1 { font-size: 1.15rem; margin-bottom: 0.6rem; }"
+      -- Mobile
+      "@media (max-width: 640px) {",
+      "  .nr-card { padding: 10px 8px; }",
+      "  .nr-card-title { font-size: 15px; }",
+      "  .nr-card-snippet { font-size: 14px; }",
+      "  .nr-article .content { font-size: 15px; line-height: 1.65; }",
+      "  .nr-subnav { padding: 4px 8px; }",
+      "}"
     ]
 
 -- ============================================================================
diff --git a/Omni/Serve.hs b/Omni/Serve.hs
index fe1415fd..03f1527e 100755
--- a/Omni/Serve.hs
+++ b/Omni/Serve.hs
@@ -46,6 +46,7 @@ import qualified Network.Wai as Wai
 import qualified Network.Wai.Handler.Warp as Warp
 import qualified Omni.Cli as Cli
 import qualified Omni.Test as Test
+import qualified Omni.Web.Style as WebStyle
 import qualified System.Directory as Dir
 import qualified System.FilePath as FP
 
@@ -127,10 +128,11 @@ renderMarkdown p' filename content = do
   let title = T.pack (FP.dropExtension filename)
       rendered = CMark.commonmarkToHtml [CMark.optSmart, CMark.optUnsafe] content
   pageShell title <| do
-    L.div_ [L.class_ "breadcrumb"] <| do
-      L.a_ [L.href_ (p' "/")] "home"
-    L.article_ [L.class_ "markdown-body"]
-      <| L.toHtmlRaw rendered
+    L.div_ [L.class_ "container"] <| do
+      L.div_ [L.class_ "breadcrumb"] <| do
+        L.a_ [L.href_ (p' "/")] "home"
+      L.article_ [L.class_ "markdown-body"]
+        <| L.toHtmlRaw rendered
 
 -- | Directory listing page.
 dirListing :: (Text -> Text) -> FilePath -> FilePath -> IO (L.Html ())
@@ -149,29 +151,30 @@ dirListing p' root relPath = do
           else Just </ Dir.getFileSize entryPath
       pure (name, isDir, modTime, size)
   pure <| pageShell title <| do
-    L.div_ [L.class_ "breadcrumb"] <| breadcrumbs p' relPath
-    L.h1_ (L.toHtml title)
-    L.table_ [L.class_ "listing"] <| do
-      L.thead_ <| L.tr_ <| do
-        L.th_ "Name"
-        L.th_ [L.class_ "size"] "Size"
-        L.th_ [L.class_ "modified"] "Modified"
-      L.tbody_ <| do
-        -- Parent directory link
-        when (relPath /= "" && relPath /= ".")
-          <| L.tr_
-          <| do
-            L.td_ <| L.a_ [L.href_ (p' (T.pack ("/" <> FP.takeDirectory relPath)))] "⬆ .."
-            L.td_ ""
-            L.td_ ""
-        forM_ items <| \(name, isDir, modTime, size) -> do
-          let href = p' (T.pack ("/" <> relPath FP.</> name <> if isDir then "/" else ""))
-              icon = if isDir then "📁 " else fileIcon name
-          L.tr_ <| do
-            L.td_ <| do
-              L.a_ [L.href_ href] <| L.toHtml (icon <> T.pack name)
-            L.td_ [L.class_ "size"] <| L.toHtml (maybe "" formatSize size)
-            L.td_ [L.class_ "modified"] <| L.toHtml (formatTime modTime)
+    L.div_ [L.class_ "container"] <| do
+      L.div_ [L.class_ "breadcrumb"] <| breadcrumbs p' relPath
+      L.h1_ (L.toHtml title)
+      L.table_ [L.class_ "listing"] <| do
+        L.thead_ <| L.tr_ <| do
+          L.th_ "Name"
+          L.th_ [L.class_ "size"] "Size"
+          L.th_ [L.class_ "modified"] "Modified"
+        L.tbody_ <| do
+          -- Parent directory link
+          when (relPath /= "" && relPath /= ".")
+            <| L.tr_
+            <| do
+              L.td_ <| L.a_ [L.href_ (p' (T.pack ("/" <> FP.takeDirectory relPath)))] "⬆ .."
+              L.td_ ""
+              L.td_ ""
+          forM_ items <| \(name, isDir, modTime, size) -> do
+            let href = p' (T.pack ("/" <> relPath FP.</> name <> if isDir then "/" else ""))
+                icon = if isDir then "📁 " else fileIcon name
+            L.tr_ <| do
+              L.td_ <| do
+                L.a_ [L.href_ href] <| L.toHtml (icon <> T.pack name)
+              L.td_ [L.class_ "size"] <| L.toHtml (maybe "" formatSize size)
+              L.td_ [L.class_ "modified"] <| L.toHtml (formatTime modTime)
 
 -- | Breadcrumb navigation.
 breadcrumbs :: (Text -> Text) -> FilePath -> L.Html ()
@@ -184,82 +187,47 @@ breadcrumbs p' relPath = do
   where
     parts = filter (/= ".") (FP.splitDirectories relPath)
 
--- | Shared Omni top navigation.
-topNav :: L.Html ()
-topNav =
-  L.nav_ [L.class_ "topnav"] <| do
-    L.a_ [L.href_ "/tasks", L.class_ "topnav-link"] "tasks"
-    L.a_ [L.href_ "/news/", L.class_ "topnav-link"] "news"
-    L.a_ [L.href_ "/files/", L.class_ "topnav-link"] "files"
-
--- | Common page shell with CSS.
+-- | Common page shell with shared design system CSS + files-specific CSS.
 pageShell :: Text -> L.Html () -> L.Html ()
-pageShell title body =
-  L.doctypehtml_ <| do
-    L.head_ <| do
-      L.meta_ [L.charset_ "utf-8"]
-      L.meta_ [L.name_ "viewport", L.content_ "width=device-width, initial-scale=1"]
-      L.title_ (L.toHtml title)
-      L.style_ css
-    L.body_ <| do
-      topNav
-      body
+pageShell title content =
+  WebStyle.sharedShell title "files" <| do
+    L.style_ filesCss
+    content
 
--- | Stylesheet.
-css :: Text
-css =
+-- | Files-specific CSS (layered on top of shared design system).
+filesCss :: Text
+filesCss =
   T.unlines
-    [ ":root { --bg: #f5f5f5; --fg: #1f2937; --muted: #6b7280; --border: #d0d0d0;",
-      "  --link: #0066cc; --code-bg: #111827; --code-fg: #d4d4d4; --hover: #f3f4f6; --card: #fff; }",
-      "@media (prefers-color-scheme: dark) {",
-      "  :root { --bg: #111827; --fg: #d4d4d4; --muted: #9ca3af; --border: #374151;",
-      "    --link: #60a5fa; --code-bg: #0b1220; --code-fg: #d4d4d4; --hover: #1f2937; --card: #111827; }",
-      "}",
-      "* { box-sizing: border-box; }",
-      "body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;",
-      "  font-size: 14px; max-width: 960px; margin: 0 auto; padding: 0.5rem 0.75rem;",
-      "  background: var(--bg); color: var(--fg); line-height: 1.4; min-height: 100vh; }",
-      "a { color: var(--link); text-decoration: none; }",
-      "a:hover { text-decoration: underline; }",
-      ".topnav { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;",
-      "  background: var(--card); border-bottom: 1px solid var(--border); margin: 0 0 0.5rem; padding: 0.375rem 0.75rem; }",
-      ".topnav-link { color: var(--muted); font-size: 0.82rem; padding: 0.25rem 0.6rem; border-radius: 2px; }",
-      ".topnav-link:hover { background: var(--hover); color: var(--fg); text-decoration: none; }",
-      ".breadcrumb { font-size: 0.8rem; color: var(--muted); margin-bottom: 0.5rem; }",
-      "h1 { font-size: 1.15rem; margin: 0 0 0.6rem; }",
+    [ ".breadcrumb { font-size: 12px; color: var(--c-fg-muted); margin-bottom: 8px; padding: 8px 16px; }",
+      ".breadcrumb a { color: var(--c-fg-faint); }",
+      ".breadcrumb a:hover { color: var(--c-accent); }",
       "",
-      "@media (max-width: 700px) {",
-      "  body { font-size: 15px; padding: 0.5rem; }",
-      "  .topnav { padding: 0.5rem; }",
-      "  .topnav-link { padding: 0.45rem 0.6rem; font-size: 0.9rem; }",
-      "  .listing td, .listing th { padding: 0.45rem 0.5rem; }",
-      "  .listing .size, .listing .modified { font-size: 0.78rem; }",
-      "  .markdown-body { padding: 0.6rem; font-size: 0.95rem; }",
-      "}",
+      ".listing { border: 1px solid var(--c-border); border-radius: 6px; overflow: hidden; }",
+      ".listing .size, .listing .modified {",
+      "  text-align: right; font-size: 11px; color: var(--c-fg-faint); white-space: nowrap; }",
       "",
-      "/* Directory listing */",
-      ".listing { width: 100%; border-collapse: collapse; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; overflow: hidden; }",
-      ".listing th { text-align: left; border-bottom: 1px solid #e5e7eb;",
-      "  padding: 0.4rem 0.65rem; font-size: 0.8rem; color: var(--muted); }",
-      ".listing td { padding: 0.38rem 0.65rem; border-bottom: 1px solid #e5e7eb; font-size: 0.86rem; }",
-      ".listing tr:last-child td { border-bottom: none; }",
-      ".listing tr:hover { background: var(--hover); }",
-      ".listing .size, .listing .modified { text-align: right;",
-      "  font-size: 0.8rem; color: var(--muted); white-space: nowrap; }",
-      "",
-      "/* Markdown */",
-      ".markdown-body { font-size: 0.92rem; background: var(--card); border: 1px solid #e5e7eb; border-radius: 2px; padding: 0.75rem; }",
-      ".markdown-body h1 { font-size: 1.4rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; }",
-      ".markdown-body h2 { font-size: 1.2rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.15rem; }",
-      ".markdown-body h3 { font-size: 1.05rem; }",
-      ".markdown-body pre { background: var(--code-bg); color: var(--code-fg); padding: 0.75rem;",
-      "  border-radius: 2px; overflow-x: auto; font-size: 0.82rem; }",
-      ".markdown-body code { background: #eef2f7; padding: 0.12rem 0.28rem; border-radius: 2px; font-size: 0.86em; }",
-      ".markdown-body pre code { background: none; padding: 0; }",
-      ".markdown-body img { max-width: 100%; height: auto; }",
+      ".markdown-body {",
+      "  font-family: var(--font-reading); font-size: 16px; line-height: 1.7;",
+      "  letter-spacing: -0.011em; max-width: 680px;",
+      "  background: var(--c-bg-raised); border: 1px solid var(--c-border);",
+      "  border-radius: 6px; padding: 16px 20px; }",
+      ".markdown-body h1 { font-size: 1.4rem; border-bottom: 1px solid var(--c-border); padding-bottom: 6px; margin: 1em 0 0.5em; }",
+      ".markdown-body h2 { font-size: 1.2rem; border-bottom: 1px solid var(--c-border-subtle); padding-bottom: 4px; margin: 0.8em 0 0.4em; }",
+      ".markdown-body h3 { font-size: 1.05rem; margin: 0.6em 0 0.3em; }",
+      ".markdown-body p { margin-bottom: 1em; }",
+      ".markdown-body pre { background: var(--c-bg); border: 1px solid var(--c-border); }",
+      ".markdown-body code { background: var(--c-bg-hover); border: 1px solid var(--c-border); }",
+      ".markdown-body pre code { background: none; border: none; padding: 0; }",
+      ".markdown-body img { max-width: 100%; height: auto; border-radius: 6px; }",
+      ".markdown-body blockquote { border-left: 3px solid var(--c-accent-dim); margin-left: 0; padding-left: 16px; color: var(--c-fg-muted); }",
       ".markdown-body table { border-collapse: collapse; width: 100%; }",
-      ".markdown-body th, .markdown-body td { border: 1px solid #e5e7eb; padding: 0.3rem 0.55rem; }",
-      ".markdown-body blockquote { border-left: 3px solid #d1d5db; margin-left: 0; padding-left: 0.75rem; color: var(--muted); }"
+      ".markdown-body th, .markdown-body td { border: 1px solid var(--c-border); padding: 6px 12px; }",
+      "",
+      "@media (max-width: 640px) {",
+      "  .listing td, .listing th { padding: 8px 8px; }",
+      "  .listing .size, .listing .modified { font-size: 10px; }",
+      "  .markdown-body { padding: 12px; font-size: 15px; }",
+      "}"
     ]
 
 -- ============================================================================
diff --git a/Omni/Task/Web/Components.hs b/Omni/Task/Web/Components.hs
index 275d542e..e9ea5608 100644
--- a/Omni/Task/Web/Components.hs
+++ b/Omni/Task/Web/Components.hs
@@ -197,8 +197,11 @@ pageHead title =
     Lucid.meta_ [Lucid.charset_ "utf-8"]
     Lucid.meta_
       [ Lucid.name_ "viewport",
-        Lucid.content_ "width=device-width, initial-scale=1"
+        Lucid.content_ "width=device-width, initial-scale=1, viewport-fit=cover"
       ]
+    Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-capable", Lucid.content_ "yes"]
+    Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-status-bar-style", Lucid.content_ "black-translucent"]
+    Lucid.meta_ [Lucid.name_ "theme-color", Lucid.content_ "#0a0a0a"]
     Lucid.link_ [Lucid.rel_ "stylesheet", Lucid.href_ "/style.css"]
     Lucid.script_
       [ Lucid.src_ "https://unpkg.com/htmx.org@2.0.4",
@@ -545,7 +548,7 @@ taskBreadcrumbs allTasks task =
   let ancestors = getAncestors allTasks task
       taskCrumbs = [Breadcrumb (TaskCore.taskId t) (Just ("/tasks/" <> TaskCore.taskId t)) | t <- List.init ancestors]
       currentCrumb = Breadcrumb (TaskCore.taskId task) Nothing
-   in [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks")]
+   in [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "tasks" (Just "/tasks")]
         ++ taskCrumbs
         ++ [currentCrumb]
 
@@ -554,7 +557,7 @@ taskBreadcrumbs allTasks task =
 navbar :: (Monad m) => Lucid.HtmlT m ()
 navbar =
   Lucid.nav_ [Lucid.class_ "navbar"] <| do
-    Lucid.a_ [Lucid.href_ "/", Lucid.class_ "navbar-brand"] "Junior"
+    Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-brand"] "omni"
     Lucid.input_
       [ Lucid.type_ "checkbox",
         Lucid.id_ "navbar-toggle",
@@ -569,22 +572,22 @@ navbar =
         Lucid.span_ [Lucid.class_ "hamburger-line"] ""
         Lucid.span_ [Lucid.class_ "hamburger-line"] ""
     Lucid.div_ [Lucid.class_ "navbar-links"] <| do
-      Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-link"] "Tasks"
-      Lucid.a_ [Lucid.href_ "/news/", Lucid.class_ "navbar-link"] "News"
-      Lucid.a_ [Lucid.href_ "/files/", Lucid.class_ "navbar-link"] "Files"
+      Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-link"] "tasks"
+      Lucid.a_ [Lucid.href_ "/news/", Lucid.class_ "navbar-link"] "news"
+      Lucid.a_ [Lucid.href_ "/files/", Lucid.class_ "navbar-link"] "files"
       Lucid.div_ [Lucid.class_ "navbar-dropdown"] <| do
-        Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "Tasks ▾"
+        Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "tasks ▾"
         Lucid.div_ [Lucid.class_ "navbar-dropdown-content"] <| do
-          Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "navbar-dropdown-item"] "Ready"
-          Lucid.a_ [Lucid.href_ "/blocked", Lucid.class_ "navbar-dropdown-item"] "Blocked"
-          Lucid.a_ [Lucid.href_ "/intervention", Lucid.class_ "navbar-dropdown-item"] "Human Action"
-          Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-dropdown-item"] "All"
+          Lucid.a_ [Lucid.href_ "/ready", Lucid.class_ "navbar-dropdown-item"] "ready"
+          Lucid.a_ [Lucid.href_ "/blocked", Lucid.class_ "navbar-dropdown-item"] "blocked"
+          Lucid.a_ [Lucid.href_ "/intervention", Lucid.class_ "navbar-dropdown-item"] "needs help"
+          Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "navbar-dropdown-item"] "all"
       Lucid.div_ [Lucid.class_ "navbar-dropdown"] <| do
-        Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "Plans ▾"
+        Lucid.button_ [Lucid.class_ "navbar-dropdown-btn"] "plans ▾"
         Lucid.div_ [Lucid.class_ "navbar-dropdown-content"] <| do
-          Lucid.a_ [Lucid.href_ "/epics", Lucid.class_ "navbar-dropdown-item"] "Epics"
-          Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "navbar-dropdown-item"] "KB"
-      Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "navbar-link"] "Stats"
+          Lucid.a_ [Lucid.href_ "/epics", Lucid.class_ "navbar-dropdown-item"] "epics"
+          Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "navbar-dropdown-item"] "kb"
+      Lucid.a_ [Lucid.href_ "/stats", Lucid.class_ "navbar-link"] "stats"
 
 -- * Badges
 
diff --git a/Omni/Task/Web/Pages.hs b/Omni/Task/Web/Pages.hs
index dd723990..5c38be90 100644
--- a/Omni/Task/Web/Pages.hs
+++ b/Omni/Task/Web/Pages.hs
@@ -16,6 +16,7 @@ import Data.Time (utctDayTime)
 import qualified Lucid
 import qualified Lucid.Base as Lucid
 import Numeric (showFFloat)
+import qualified Omni.Task.Core as TaskCore
 import Omni.Task.Web.Components
   ( Breadcrumb (..),
     complexityBadgeWithForm,
@@ -66,7 +67,6 @@ import Omni.Task.Web.Types
     sortTasks,
     timeRangeToParam,
   )
-import qualified Omni.Task.Core as TaskCore
 
 taskToUnixTs :: TaskCore.Task -> Int
 taskToUnixTs t =
@@ -177,9 +177,9 @@ instance Lucid.ToHtml HomePage where
 instance Lucid.ToHtml ReadyQueuePage where
   toHtmlRaw = Lucid.toHtml
   toHtml (ReadyQueuePage tasks currentSort _now) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Ready Queue" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Ready Queue" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Ready Queue - Jr"
+          pageHead "Ready Queue - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -192,9 +192,9 @@ instance Lucid.ToHtml ReadyQueuePage where
 instance Lucid.ToHtml BlockedPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (BlockedPage tasksWithImpact currentSort _now) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Blocked" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Blocked" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Blocked Tasks - Jr"
+          pageHead "Blocked Tasks - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -208,13 +208,13 @@ instance Lucid.ToHtml BlockedPage where
 instance Lucid.ToHtml InterventionPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (InterventionPage actionItems currentSort _now) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Needs Human Action" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Needs Human Action" Nothing]
         failed = TaskCore.failedTasks actionItems
         epicsReady = TaskCore.epicsInReview actionItems
         needsHelp = TaskCore.tasksNeedingHelp actionItems
         totalCount = length failed + length epicsReady + length needsHelp
      in Lucid.doctypehtml_ <| do
-          pageHead "Needs Human Action - Jr"
+          pageHead "Needs Human Action - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -239,9 +239,9 @@ instance Lucid.ToHtml InterventionPage where
 instance Lucid.ToHtml KBPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (KBPage facts) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Knowledge Base - Jr"
+          pageHead "Knowledge Base - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ "Knowledge Base"
@@ -336,9 +336,9 @@ instance Lucid.ToHtml KBPage where
 instance Lucid.ToHtml FactDetailPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (FactDetailNotFound fid) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> tshow fid) Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> tshow fid) Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Fact Not Found - Jr"
+          pageHead "Fact Not Found - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ "Fact Not Found"
@@ -346,9 +346,9 @@ instance Lucid.ToHtml FactDetailPage where
               Lucid.a_ [Lucid.href_ "/kb", Lucid.class_ "btn btn-secondary"] "Back to Knowledge Base"
   toHtml (FactDetailFound fact now) =
     let fid' = maybe "-" tshow (TaskCore.factId fact)
-        crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> fid') Nothing]
+        crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Knowledge Base" (Just "/kb"), Breadcrumb ("Fact #" <> fid') Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Fact Detail - Jr"
+          pageHead "Fact Detail - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "task-detail-header"] <| do
@@ -439,9 +439,9 @@ instance Lucid.ToHtml FactDetailPage where
 instance Lucid.ToHtml EpicsPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (EpicsPage epics allTasks currentSort) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Epics" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Epics" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Epics - Jr"
+          pageHead "Epics - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -455,9 +455,9 @@ instance Lucid.ToHtml EpicsPage where
 instance Lucid.ToHtml TaskListPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (TaskListPage tasks filters currentSort _now) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Tasks - Jr"
+          pageHead "Tasks - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "page-header-row"] <| do
@@ -537,9 +537,9 @@ instance Lucid.ToHtml TaskListPage where
 instance Lucid.ToHtml TaskDetailPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (TaskDetailNotFound tid) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Task Not Found - Jr"
+          pageHead "Task Not Found - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ "Task Not Found"
@@ -550,7 +550,7 @@ instance Lucid.ToHtml TaskDetailPage where
   toHtml (TaskDetailFound task allTasks _activities maybeRetry commits maybeAggMetrics agentEvents now) =
     let crumbs = taskBreadcrumbs allTasks task
      in Lucid.doctypehtml_ <| do
-          pageHead (TaskCore.taskId task <> " - Jr")
+          pageHead (TaskCore.taskId task <> " - omni")
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ <| Lucid.toHtml (TaskCore.taskTitle task)
@@ -655,7 +655,7 @@ instance Lucid.ToHtml TaskDetailPage where
 instance Lucid.ToHtml TaskReviewPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (ReviewPageNotFound tid) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
      in Lucid.doctypehtml_ <| do
           pageHead "Task Not Found - Jr Review"
           pageBodyWithCrumbs crumbs <| do
@@ -667,9 +667,9 @@ instance Lucid.ToHtml TaskReviewPage where
                 " could not be found."
   toHtml (ReviewPageFound task reviewInfo) =
     let tid = TaskCore.taskId task
-        crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
+        crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb "Review" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead ("Review: " <> TaskCore.taskId task <> " - Jr")
+          pageHead ("Review: " <> TaskCore.taskId task <> " - omni")
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ "Review Task"
@@ -734,9 +734,9 @@ instance Lucid.ToHtml TaskDiffPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (DiffPageNotFound tid commitHash') =
     let shortHash = Text.take 8 commitHash'
-        crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+        crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Commit Not Found - Jr"
+          pageHead "Commit Not Found - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ "Commit Not Found"
@@ -746,9 +746,9 @@ instance Lucid.ToHtml TaskDiffPage where
               Lucid.a_ [Lucid.href_ ("/tasks/" <> tid), Lucid.class_ "back-link"] "← Back to task"
   toHtml (DiffPageFound tid commitHash' diffOutput) =
     let shortHash = Text.take 8 commitHash'
-        crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
+        crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Tasks" (Just "/tasks"), Breadcrumb tid (Just ("/tasks/" <> tid)), Breadcrumb ("Diff " <> shortHash) Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead ("Diff " <> shortHash <> " - Jr")
+          pageHead ("Diff " <> shortHash <> " - omni")
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.div_ [Lucid.class_ "diff-header"] <| do
@@ -761,9 +761,9 @@ instance Lucid.ToHtml TaskDiffPage where
 instance Lucid.ToHtml StatsPage where
   toHtmlRaw = Lucid.toHtml
   toHtml (StatsPage stats maybeEpic) =
-    let crumbs = [Breadcrumb "Jr" (Just "/"), Breadcrumb "Stats" Nothing]
+    let crumbs = [Breadcrumb "omni" (Just "/tasks"), Breadcrumb "Stats" Nothing]
      in Lucid.doctypehtml_ <| do
-          pageHead "Task Statistics - Jr"
+          pageHead "Task Statistics - omni"
           pageBodyWithCrumbs crumbs <| do
             Lucid.div_ [Lucid.class_ "container"] <| do
               Lucid.h1_ <| case maybeEpic of
diff --git a/Omni/Task/Web/Style.hs b/Omni/Task/Web/Style.hs
index d89de5f0..e41d996f 100644
--- a/Omni/Task/Web/Style.hs
+++ b/Omni/Task/Web/Style.hs
@@ -9,15 +9,16 @@ module Omni.Task.Web.Style
   )
 where
 
-import Alpha hiding (wrap, (**), (|>))
+import Alpha hiding (rem, wrap, (**), (|>))
 import Clay
 import qualified Clay.Flexbox as Flexbox
 import qualified Clay.Media as Media
 import qualified Clay.Stylesheet as Stylesheet
 import qualified Data.Text.Lazy as LazyText
+import qualified Omni.Web.Style as WebStyle
 
 css :: LazyText.Text
-css = render stylesheet
+css = WebStyle.css <> render stylesheet
 
 stylesheet :: Css
 stylesheet = do
@@ -46,26 +47,14 @@ stylesheet = do
 
 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)
+  -- The shared Omni.Web.Style reset already covers *, html, body basics.
+  -- Here we layer Task-specific overrides.
   body ? do
-    fontFamily
-      [ "-apple-system",
-        "BlinkMacSystemFont",
-        "Segoe UI",
-        "Roboto",
-        "Helvetica Neue",
-        "Arial",
-        "Noto Sans",
-        "sans-serif"
-      ]
-      [sansSerif]
-    fontSize (px 14)
-    lineHeight (em 1.3)
-    color "#1f2937"
-    backgroundColor "#f5f5f5"
+    fontFamily WebStyle.fontMono [monospace]
+    fontSize (px 13)
+    lineHeight (unitless 1.6)
+    color WebStyle.cFg
+    backgroundColor WebStyle.cBg
     minHeight (vh 100)
   "h1" ? do
     fontSize (px 20)
@@ -74,30 +63,34 @@ baseStyles = do
   "h2" ? do
     fontSize (px 16)
     fontWeight (weight 600)
-    color "#374151"
+    color WebStyle.cFg
     margin (em 1) (px 0) (em 0.5) (px 0)
   "h3" ? do
     fontSize (px 14)
     fontWeight (weight 600)
-    color "#374151"
+    color WebStyle.cFg
     margin (em 0.75) (px 0) (em 0.25) (px 0)
   a ? do
-    color "#0066cc"
+    color WebStyle.cLink
     textDecoration none
-  a # hover ? textDecoration underline
+  a # hover ? do
+    textDecoration underline
+    Stylesheet.key "text-underline-offset" ("2px" :: Text)
   code ? do
-    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+    fontFamily WebStyle.fontMono [monospace]
     fontSize (em 0.9)
-    backgroundColor "#f3f4f6"
-    padding (px 1) (px 4) (px 1) (px 4)
-    borderRadius (px 2) (px 2) (px 2) (px 2)
+    backgroundColor WebStyle.cBgHover
+    padding (px 1) (px 5) (px 1) (px 5)
+    borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+    border (px 1) solid WebStyle.cBorder
   pre ? do
-    fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
+    fontFamily WebStyle.fontMono [monospace]
     fontSize (px 12)
-    backgroundColor "#1e1e1e"
-    color "#d4d4d4"
-    padding (px 8) (px 8) (px 8) (px 8)
-    borderRadius (px 2) (px 2) (px 2) (px 2)
+    backgroundColor WebStyle.cBg
+    color WebStyle.cFg
+    border (px 1) solid WebStyle.cBorder
+    padding (px 8) (px 16) (px 8) (px 16)
+    borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
     overflow auto
     whiteSpace preWrap
     maxHeight (px 500)
@@ -108,7 +101,7 @@ layoutStyles = do
     width (pct 100)
     maxWidth (px 960)
     margin (px 0) auto (px 0) auto
-    padding (px 8) (px 12) (px 8) (px 12)
+    padding (px 8) (px 16) (px 8) (px 16)
   main_ ? do
     Stylesheet.key "flex" ("1 0 auto" :: Text)
   ".page-content" ? do
@@ -129,7 +122,7 @@ layoutStyles = do
   ".detail-label" ? do
     fontWeight (weight 600)
     width (px 100)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     minWidth (px 80)
     fontSize (px 13)
   ".detail-value" ? do
@@ -138,37 +131,44 @@ layoutStyles = do
   ".detail-section" ? do
     marginTop (em 0.75)
     paddingTop (em 0.75)
-    borderTop (px 1) solid "#e5e7eb"
+    borderTop (px 1) solid WebStyle.cBorderSubtle
   ".dep-list" <> ".child-list" ? do
     margin (px 4) (px 0) (px 4) (px 0)
     paddingLeft (px 16)
   (".dep-list" ** li) <> (".child-list" ** li) ? margin (px 2) (px 0) (px 2) (px 0)
   ".dep-type" <> ".child-status" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontSize (px 12)
-  ".child-title" ? color "#374151"
+  ".child-title" ? color WebStyle.cFg
   ".priority-desc" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     marginLeft (px 4)
 
 navigationStyles :: Css
 navigationStyles = do
   ".navbar" ? do
-    backgroundColor white
-    padding (px 6) (px 12) (px 6) (px 12)
-    borderBottom (px 1) solid "#d0d0d0"
-    marginBottom (px 8)
+    backgroundColor WebStyle.cBg
+    padding (px 8) (px 16) (px 8) (px 16)
+    borderBottom (px 1) solid WebStyle.cBorder
     display flex
     alignItems center
     justifyContent spaceBetween
     flexWrap Flexbox.wrap
     Stylesheet.key "gap" ("8px" :: Text)
+    position sticky
+    top nil
+    zIndex 100
+    Stylesheet.key "backdrop-filter" ("blur(12px)" :: Text)
+    Stylesheet.key "-webkit-backdrop-filter" ("blur(12px)" :: Text)
   ".navbar-brand" ? do
-    fontSize (px 18)
+    fontSize (px 14)
     fontWeight bold
-    color "#0066cc"
+    color WebStyle.cAccent
     textDecoration none
-  ".navbar-brand" # hover ? textDecoration none
+    Stylesheet.key "letter-spacing" ("0.05em" :: Text)
+  ".navbar-brand" # hover ? do
+    textDecoration none
+    opacity 0.8
   ".navbar-toggle-checkbox" ? display none
   ".navbar-hamburger" ? do
     display none
@@ -183,7 +183,7 @@ navigationStyles = do
     display block
     width (px 20)
     height (px 2)
-    backgroundColor "#374151"
+    backgroundColor WebStyle.cFgMuted
     borderRadius (px 1) (px 1) (px 1) (px 1)
     transition "all" (ms 200) ease (sec 0)
   ".navbar-links" ? do
@@ -194,14 +194,15 @@ navigationStyles = do
   ".navbar-link" ? do
     display inlineBlock
     padding (px 4) (px 10) (px 4) (px 10)
-    color "#374151"
+    color WebStyle.cFgMuted
     textDecoration none
-    borderRadius (px 2) (px 2) (px 2) (px 2)
-    fontSize (px 13)
+    borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+    fontSize (px 12)
     fontWeight (weight 500)
-    transition "background-color" (ms 150) ease (sec 0)
+    transition "all" (ms 150) ease (sec 0)
   ".navbar-link" # hover ? do
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFg
     textDecoration none
   ".navbar-dropdown" ? do
     position relative
@@ -209,24 +210,28 @@ navigationStyles = do
   ".navbar-dropdown-btn" ? do
     display inlineBlock
     padding (px 4) (px 10) (px 4) (px 10)
-    color "#374151"
+    color WebStyle.cFgMuted
     backgroundColor transparent
     border (px 0) none transparent
-    borderRadius (px 2) (px 2) (px 2) (px 2)
-    fontSize (px 13)
+    borderRadius WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm WebStyle.radiusSm
+    fontSize (px 12)
     fontWeight (weight 500)
     cursor pointer
-    transition "background-color" (ms 150) ease (sec 0)
-  ".navbar-dropdown-btn" # hover ? backgroundColor "#f3f4f6"
+    fontFamily WebStyle.fontMono [monospace]
+    transition "all" (ms 150) ease (sec 0)
+  ".navbar-dropdown-btn" # hover ? do
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFg
   ".navbar-dropdown-content" ? do
     display none
     position absolute
     left (px 0)
     top (pct 100)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     minWidth (px 120)
-    Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
-    borderRadius (px 2) (px 2) (px 2) (px 2)
+    Stylesheet.key "box-shadow" ("0 4px 16px rgba(0,0,0,0.4)" :: Text)
+    borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+    border (px 1) solid WebStyle.cBorder
     zIndex 100
     Stylesheet.key "overflow" ("hidden" :: Text)
   ".navbar-dropdown" # hover |> ".navbar-dropdown-content" ? display block
@@ -234,18 +239,18 @@ navigationStyles = do
   ".navbar-dropdown-item" ? do
     display block
     padding (px 8) (px 12) (px 8) (px 12)
-    color "#374151"
+    color WebStyle.cFgMuted
     textDecoration none
-    fontSize (px 13)
-    transition "background-color" (ms 150) ease (sec 0)
+    fontSize (px 12)
+    transition "all" (ms 150) ease (sec 0)
   ".navbar-dropdown-item" # hover ? do
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFg
     textDecoration none
   header ? do
-    backgroundColor white
-    padding (px 6) (px 12) (px 6) (px 12)
-    borderBottom (px 1) solid "#d0d0d0"
-    marginBottom (px 8)
+    backgroundColor WebStyle.cBg
+    padding (px 8) (px 16) (px 8) (px 16)
+    borderBottom (px 1) solid WebStyle.cBorder
   ".nav-content" ? do
     maxWidth (px 960)
     margin (px 0) auto (px 0) auto
@@ -255,9 +260,9 @@ navigationStyles = do
     flexWrap Flexbox.wrap
     Stylesheet.key "gap" ("8px" :: Text)
   ".nav-brand" ? do
-    fontSize (px 16)
+    fontSize (px 14)
     fontWeight bold
-    color "#1f2937"
+    color WebStyle.cAccent
     textDecoration none
   ".nav-brand" # hover ? textDecoration none
   ".nav-links" ? do
@@ -290,13 +295,13 @@ breadcrumbStyles = do
     alignItems center
     Stylesheet.key "gap" ("4px" :: Text)
   ".breadcrumb-sep" ? do
-    color "#9ca3af"
+    color WebStyle.cFgFaint
     Stylesheet.key "user-select" ("none" :: Text)
   ".breadcrumb-current" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontWeight (weight 500)
   (".breadcrumb-list" ** a) ? do
-    color "#0066cc"
+    color WebStyle.cAccent
     textDecoration none
   (".breadcrumb-list" ** a) # hover ? textDecoration underline
 
@@ -312,10 +317,10 @@ cardStyles = do
     <> ".diff-section"
     <> ".review-actions"
     ? do
-      backgroundColor white
-      borderRadius (px 2) (px 2) (px 2) (px 2)
-      padding (px 8) (px 10) (px 8) (px 10)
-      border (px 1) solid "#d0d0d0"
+      backgroundColor WebStyle.cBgRaised
+      borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+      padding (px 8) (px 12) (px 8) (px 12)
+      border (px 1) solid WebStyle.cBorder
   ".review-actions" ? do
     display flex
     flexDirection row
@@ -328,25 +333,25 @@ cardStyles = do
     fontWeight bold
   ".stat-label" ? do
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     marginTop (px 2)
   ".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"
-  ".stat-card.badge-neutral" ? borderLeft (px 4) solid "#6b7280"
-  (".stat-card.badge-neutral" |> ".stat-count") ? color "#374151"
+    borderLeft (px 4) solid WebStyle.cYellow
+  (".stat-card.badge-open" |> ".stat-count") ? color WebStyle.cYellow
+  ".stat-card.badge-inprogress" ? borderLeft (px 4) solid WebStyle.cBlue
+  (".stat-card.badge-inprogress" |> ".stat-count") ? color WebStyle.cBlue
+  ".stat-card.badge-review" ? borderLeft (px 4) solid WebStyle.cPurple
+  (".stat-card.badge-review" |> ".stat-count") ? color WebStyle.cPurple
+  ".stat-card.badge-approved" ? borderLeft (px 4) solid WebStyle.cAccent
+  (".stat-card.badge-approved" |> ".stat-count") ? color WebStyle.cAccent
+  ".stat-card.badge-done" ? borderLeft (px 4) solid WebStyle.cGreen
+  (".stat-card.badge-done" |> ".stat-count") ? color WebStyle.cGreen
+  ".stat-card.badge-neutral" ? borderLeft (px 4) solid WebStyle.cFgMuted
+  (".stat-card.badge-neutral" |> ".stat-count") ? color WebStyle.cFgMuted
   ".task-card" ? do
     transition "border-color" (ms 150) ease (sec 0)
   ".task-card" # hover ? do
-    borderColor "#999"
+    borderColor WebStyle.cAccentDim
   ".task-card-link" ? do
     display block
     textDecoration none
@@ -360,19 +365,19 @@ cardStyles = do
     Stylesheet.key "gap" ("6px" :: Text)
     marginBottom (px 4)
   ".task-id" ? do
-    fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
-    color "#0066cc"
+    fontFamily WebStyle.fontMono [monospace]
+    color WebStyle.cAccent
     textDecoration none
     fontSize (px 12)
     padding (px 2) (px 0) (px 2) (px 0)
   ".task-id" # hover ? textDecoration underline
   ".priority" ? do
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".blocking-impact" ? do
     fontSize (px 10)
-    color "#6b7280"
-    backgroundColor "#e5e7eb"
+    color WebStyle.cFgMuted
+    backgroundColor WebStyle.cBorder
     padding (px 1) (px 6) (px 1) (px 6)
     borderRadius (px 8) (px 8) (px 8) (px 8)
     marginLeft auto
@@ -380,33 +385,33 @@ cardStyles = do
     fontSize (px 14)
     margin (px 0) (px 0) (px 0) (px 0)
   ".empty-msg" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontStyle italic
   ".info-msg" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     marginBottom (px 12)
   ".kb-preview" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontSize (px 12)
     marginTop (px 4)
     overflow hidden
     Stylesheet.key "text-overflow" ("ellipsis" :: Text)
   ".ready-link" ? do
     fontSize (px 13)
-    color "#0066cc"
+    color WebStyle.cAccent
   ".count-badge" ? do
-    backgroundColor "#0066cc"
+    backgroundColor WebStyle.cAccentDim
     color white
     padding (px 2) (px 8) (px 2) (px 8)
     borderRadius (px 10) (px 10) (px 10) (px 10)
     fontSize (px 12)
     verticalAlign middle
   ".description" ? do
-    backgroundColor "#f9fafb"
+    backgroundColor WebStyle.cBgRaised
     padding (px 8) (px 8) (px 8) (px 8)
     borderRadius (px 2) (px 2) (px 2) (px 2)
     margin (px 0) (px 0) (px 0) (px 0)
-    color "#374151"
+    color WebStyle.cFg
     fontSize (px 13)
   ".description-block" ? do
     pure ()
@@ -419,9 +424,9 @@ cardStyles = do
     margin (px 0) (px 0) (px 0) (px 0)
   ".edit-link" <> ".cancel-link" ? do
     fontSize (px 12)
-    color "#0066cc"
+    color WebStyle.cAccent
   "button.cancel-link" ? do
-    color "#dc2626"
+    color WebStyle.cRed
     backgroundColor transparent
     border (px 0) solid transparent
     padding (px 0) (px 0) (px 0) (px 0)
@@ -432,13 +437,13 @@ cardStyles = do
     overflowY auto
   ".progress-bar" ? do
     height (px 6)
-    backgroundColor "#e5e7eb"
+    backgroundColor WebStyle.cBorder
     borderRadius (px 2) (px 2) (px 2) (px 2)
     overflow hidden
     marginTop (px 6)
   ".progress-fill" ? do
     height (pct 100)
-    backgroundColor "#0066cc"
+    backgroundColor WebStyle.cAccentDim
     borderRadius (px 2) (px 2) (px 2) (px 2)
     transition "width" (ms 300) ease (sec 0)
   ".multi-progress-container" ? do
@@ -446,22 +451,22 @@ cardStyles = do
   ".multi-progress-bar" ? do
     display flex
     height (px 8)
-    backgroundColor "#e5e7eb"
+    backgroundColor WebStyle.cBorder
     borderRadius (px 4) (px 4) (px 4) (px 4)
     overflow hidden
     marginTop (px 6)
   ".multi-progress-segment" ? do
     height (pct 100)
     transition "width" (ms 300) ease (sec 0)
-  ".progress-done" ? backgroundColor "#10b981"
-  ".progress-inprogress" ? backgroundColor "#f59e0b"
-  ".progress-open" ? backgroundColor "#3b82f6"
+  ".progress-done" ? backgroundColor WebStyle.cGreen
+  ".progress-inprogress" ? backgroundColor WebStyle.cYellow
+  ".progress-open" ? backgroundColor WebStyle.cBlue
   ".progress-legend" ? do
     display flex
     Stylesheet.key "gap" ("16px" :: Text)
     marginTop (px 6)
     fontSize (px 12)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".legend-item" ? do
     display flex
     alignItems center
@@ -471,14 +476,14 @@ cardStyles = do
     width (px 10)
     height (px 10)
     borderRadius (px 2) (px 2) (px 2) (px 2)
-  ".legend-done" ? backgroundColor "#10b981"
-  ".legend-inprogress" ? backgroundColor "#f59e0b"
-  ".legend-open" ? backgroundColor "#3b82f6"
+  ".legend-done" ? backgroundColor WebStyle.cGreen
+  ".legend-inprogress" ? backgroundColor WebStyle.cYellow
+  ".legend-open" ? backgroundColor WebStyle.cBlue
   ".stats-section" ? do
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
   ".stats-row" ? do
     display flex
     alignItems center
@@ -497,19 +502,19 @@ cardStyles = do
     fontWeight (weight 500)
     fontSize (px 13)
   ".summary-section" ? do
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
   ".no-commit-msg" ? do
-    backgroundColor "#fff3cd"
-    border (px 1) solid "#ffc107"
+    Stylesheet.key "background" ("rgba(250,204,21,0.1)" :: Text)
+    border (px 1) solid WebStyle.cYellow
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
     margin (px 8) (px 0) (px 8) (px 0)
   ".conflict-warning" ? do
-    backgroundColor "#fee2e2"
-    border (px 1) solid "#ef4444"
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+    border (px 1) solid WebStyle.cRed
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
     margin (px 8) (px 0) (px 8) (px 0)
@@ -519,22 +524,22 @@ listGroupStyles = do
   ".list-group" ? do
     display flex
     flexDirection column
-    backgroundColor white
-    borderRadius (px 2) (px 2) (px 2) (px 2)
-    border (px 1) solid "#d0d0d0"
+    backgroundColor WebStyle.cBgRaised
+    borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
+    border (px 1) solid WebStyle.cBorder
     overflow hidden
   ".list-group-item" ? do
     display flex
     alignItems center
     justifyContent spaceBetween
     padding (px 8) (px 10) (px 8) (px 10)
-    borderBottom (px 1) solid "#e5e7eb"
+    borderBottom (px 1) solid WebStyle.cBorderSubtle
     textDecoration none
     color inherit
     transition "background-color" (ms 150) ease (sec 0)
   ".list-group-item" # lastChild ? borderBottom (px 0) none transparent
   ".list-group-item" # hover ? do
-    backgroundColor "#f9fafb"
+    backgroundColor WebStyle.cBgHover
     textDecoration none
   ".list-group-item-content" ? do
     display flex
@@ -545,12 +550,12 @@ listGroupStyles = do
     overflow hidden
   ".list-group-item-id" ? do
     fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
-    color "#0066cc"
+    color WebStyle.cAccent
     fontSize (px 12)
     flexShrink 0
   ".list-group-item-title" ? do
     fontSize (px 13)
-    color "#374151"
+    color WebStyle.cFg
     overflow hidden
     Stylesheet.key "text-overflow" ("ellipsis" :: Text)
     whiteSpace nowrap
@@ -570,23 +575,23 @@ statusBadges = do
     fontWeight (weight 500)
     whiteSpace nowrap
   ".badge-open" ? do
-    backgroundColor "#fef3c7"
-    color "#92400e"
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    color WebStyle.cYellow
   ".badge-inprogress" ? do
-    backgroundColor "#dbeafe"
-    color "#1e40af"
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+    color WebStyle.cBlue
   ".badge-review" ? do
-    backgroundColor "#ede9fe"
-    color "#6b21a8"
+    Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+    color WebStyle.cPurple
   ".badge-approved" ? do
-    backgroundColor "#cffafe"
-    color "#0e7490"
+    Stylesheet.key "background" ("rgba(34,211,238,0.15)" :: Text)
+    color WebStyle.cAccent
   ".badge-done" ? do
-    backgroundColor "#d1fae5"
-    color "#065f46"
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+    color WebStyle.cGreen
   ".badge-needshelp" ? do
-    backgroundColor "#fef3c7"
-    color "#92400e"
+    Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+    color WebStyle.cOrange
   ".status-badge-dropdown" ? do
     position relative
     display inlineBlock
@@ -605,7 +610,7 @@ statusBadges = do
     left (px 0)
     top (pct 100)
     marginTop (px 2)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 4) (px 4) (px 4) (px 4)
     Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
     zIndex 100
@@ -637,20 +642,20 @@ statusBadges = do
     Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
     Stylesheet.key "outline-offset" ("2px" :: Text)
   ".badge-p0" ? do
-    backgroundColor "#fee2e2"
-    color "#991b1b"
+    Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+    color WebStyle.cRed
   ".badge-p1" ? do
-    backgroundColor "#fef3c7"
-    color "#92400e"
+    Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+    color WebStyle.cOrange
   ".badge-p2" ? do
-    backgroundColor "#dbeafe"
-    color "#1e40af"
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+    color WebStyle.cBlue
   ".badge-p3" ? do
-    backgroundColor "#e5e7eb"
-    color "#4b5563"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFgMuted
   ".badge-p4" ? do
-    backgroundColor "#f3f4f6"
-    color "#6b7280"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFgFaint
   ".priority-badge-dropdown" ? do
     position relative
     display inlineBlock
@@ -665,7 +670,7 @@ statusBadges = do
     left (px 0)
     top (pct 100)
     marginTop (px 2)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 4) (px 4) (px 4) (px 4)
     Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
     zIndex 100
@@ -697,26 +702,26 @@ statusBadges = do
     Stylesheet.key "outline" ("2px solid #0066cc" :: Text)
     Stylesheet.key "outline-offset" ("2px" :: Text)
   ".badge-complexity" ? do
-    backgroundColor "#f0f9ff"
-    color "#0c4a6e"
+    Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
+    color WebStyle.cAccentDim
   ".badge-complexity-1" ? do
-    backgroundColor "#f0fdf4"
-    color "#166534"
+    Stylesheet.key "background" ("rgba(74,222,128,0.1)" :: Text)
+    color WebStyle.cGreen
   ".badge-complexity-2" ? do
-    backgroundColor "#f0f9ff"
-    color "#075985"
+    Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
+    color WebStyle.cBlue
   ".badge-complexity-3" ? do
-    backgroundColor "#fef3c7"
-    color "#92400e"
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    color WebStyle.cYellow
   ".badge-complexity-4" ? do
-    backgroundColor "#fef3c7"
-    color "#b45309"
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    color WebStyle.cOrange
   ".badge-complexity-5" ? do
-    backgroundColor "#fee2e2"
-    color "#991b1b"
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+    color WebStyle.cRed
   ".badge-complexity-none" ? do
-    backgroundColor "#f3f4f6"
-    color "#6b7280"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFgMuted
   ".complexity-badge-dropdown" ? do
     position relative
     display inlineBlock
@@ -731,7 +736,7 @@ statusBadges = do
     left (px 0)
     top (pct 100)
     marginTop (px 2)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 4) (px 4) (px 4) (px 4)
     Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.15)" :: Text)
     zIndex 100
@@ -786,16 +791,16 @@ buttonStyles = do
       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"
+    backgroundColor WebStyle.cBgRaised
+    border (px 1) solid WebStyle.cBorder
+    color WebStyle.cFg
   ".action-btn" # hover ? do
-    backgroundColor "#f9fafb"
-    borderColor "#9ca3af"
+    backgroundColor WebStyle.cBgHover
+    borderColor WebStyle.cFgFaint
   ".action-btn-primary" <> ".filter-btn" <> ".submit-btn" ? do
-    backgroundColor "#0066cc"
+    backgroundColor WebStyle.cAccentDim
     color white
-    borderColor "#0066cc"
+    borderColor WebStyle.cAccentDim
   ".action-btn-primary"
     # hover
     <> ".filter-btn"
@@ -803,37 +808,39 @@ buttonStyles = do
     <> ".submit-btn"
     # hover
     ? do
-      backgroundColor "#0052a3"
+      backgroundColor WebStyle.cAccent
+      color black
   ".accept-btn" ? do
-    backgroundColor "#10b981"
-    color white
-  ".accept-btn" # hover ? backgroundColor "#059669"
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+    color WebStyle.cGreen
+  ".accept-btn" # hover ? Stylesheet.key "background" ("rgba(74,222,128,0.25)" :: Text)
   ".reject-btn" ? do
-    backgroundColor "#ef4444"
-    color white
-  ".reject-btn" # hover ? backgroundColor "#dc2626"
+    Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+    color WebStyle.cRed
+  ".reject-btn" # hover ? Stylesheet.key "background" ("rgba(248,113,113,0.25)" :: Text)
   ".clear-btn" ? do
     display inlineBlock
     minHeight (px 36)
     padding (px 6) (px 10) (px 6) (px 10)
-    backgroundColor "#6b7280"
-    color white
-    borderRadius (px 2) (px 2) (px 2) (px 2)
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFg
+    border (px 1) solid WebStyle.cBorder
+    borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
     textDecoration none
     fontSize (px 13)
     cursor pointer
-  ".clear-btn" # hover ? backgroundColor "#4b5563"
+  ".clear-btn" # hover ? backgroundColor WebStyle.cBgActive
   ".review-link-btn" ? do
-    backgroundColor "#8b5cf6"
-    color white
-  ".review-link-btn" # hover ? backgroundColor "#7c3aed"
+    Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+    color WebStyle.cPurple
+  ".review-link-btn" # hover ? Stylesheet.key "background" ("rgba(167,139,250,0.25)" :: Text)
   ".review-link-section" ? margin (px 8) (px 0) (px 8) (px 0)
   ".btn-secondary" <> ".load-more-btn" ? do
-    backgroundColor "#6b7280"
+    backgroundColor WebStyle.cFgMuted
     color white
     width (pct 100)
     marginTop (px 8)
-  ".btn-secondary" # hover <> ".load-more-btn" # hover ? backgroundColor "#4b5563"
+  ".btn-secondary" # hover <> ".load-more-btn" # hover ? backgroundColor WebStyle.cFgFaint
 
 formStyles :: Css
 formStyles = do
@@ -849,15 +856,18 @@ formStyles = do
     Stylesheet.key "gap" ("4px" :: Text)
   (".filter-group" |> label) ? do
     fontSize (px 12)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontWeight (weight 500)
     whiteSpace nowrap
   ".filter-select" <> ".filter-input" <> ".status-select" ? do
     minHeight (px 36)
     padding (px 6) (px 10) (px 6) (px 10)
-    border (px 1) solid "#d1d5db"
-    borderRadius (px 2) (px 2) (px 2) (px 2)
+    border (px 1) solid WebStyle.cBorder
+    borderRadius WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd WebStyle.radiusMd
     fontSize (px 13)
+    fontFamily WebStyle.fontMono [monospace]
+    backgroundColor WebStyle.cBg
+    color WebStyle.cFg
     minWidth (px 100)
   ".filter-input" ? minWidth (px 120)
   ".inline-form" ? display inlineBlock
@@ -872,17 +882,17 @@ formStyles = do
     minWidth (px 160)
     minHeight (px 36)
     padding (px 6) (px 10) (px 6) (px 10)
-    border (px 1) solid "#d1d5db"
+    border (px 1) solid WebStyle.cBorder
     borderRadius (px 2) (px 2) (px 2) (px 2)
     fontSize (px 13)
     Stylesheet.key "resize" ("vertical" :: Text)
   ".edit-description" ? do
     marginTop (px 8)
     padding (px 8) (px 0) (px 0) (px 0)
-    borderTop (px 1) solid "#e5e7eb"
+    borderTop (px 1) solid WebStyle.cBorderSubtle
   (".edit-description" |> "summary") ? do
     cursor pointer
-    color "#0066cc"
+    color WebStyle.cAccent
     fontSize (px 13)
     fontWeight (weight 500)
   (".edit-description" |> "summary") # hover ? textDecoration underline
@@ -890,7 +900,7 @@ formStyles = do
     width (pct 100)
     minHeight (px 250)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d1d5db"
+    border (px 1) solid WebStyle.cBorder
     borderRadius (px 2) (px 2) (px 2) (px 2)
     fontSize (px 13)
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
@@ -912,16 +922,16 @@ formStyles = do
     marginBottom (px 4)
     fontSize (px 13)
     fontWeight (weight 500)
-    color "#374151"
+    color WebStyle.cFg
   ".form-input" <> ".form-textarea" ? do
     width (pct 100)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d1d5db"
+    border (px 1) solid WebStyle.cBorder
     borderRadius (px 2) (px 2) (px 2) (px 2)
     fontSize (px 14)
     lineHeight (em 1.5)
   ".form-input" # focus <> ".form-textarea" # focus ? do
-    borderColor "#0066cc"
+    borderColor WebStyle.cAccentDim
     Stylesheet.key "outline" ("none" :: Text)
     Stylesheet.key "box-shadow" ("0 0 0 2px rgba(0, 102, 204, 0.2)" :: Text)
   ".form-textarea" ? do
@@ -948,47 +958,47 @@ formStyles = do
     cursor pointer
     transition "all" (ms 150) ease (sec 0)
   ".btn-primary" ? do
-    backgroundColor "#0066cc"
+    backgroundColor WebStyle.cAccentDim
     color white
-  ".btn-primary" # hover ? backgroundColor "#0052a3"
+  ".btn-primary" # hover ? backgroundColor WebStyle.cAccent
   ".btn-secondary" ? do
-    backgroundColor "#6b7280"
+    backgroundColor WebStyle.cFgMuted
     color white
-  ".btn-secondary" # hover ? backgroundColor "#4b5563"
+  ".btn-secondary" # hover ? backgroundColor WebStyle.cFgFaint
   ".btn-danger" ? do
-    backgroundColor "#dc2626"
+    backgroundColor WebStyle.cRed
     color white
-  ".btn-danger" # hover ? backgroundColor "#b91c1c"
+  ".btn-danger" # hover ? backgroundColor WebStyle.cRed
   ".danger-zone" ? do
     marginTop (px 24)
     padding (px 16) (px 16) (px 16) (px 16)
-    backgroundColor "#fef2f2"
-    border (px 1) solid "#fecaca"
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+    border (px 1) solid WebStyle.cRed
     borderRadius (px 4) (px 4) (px 4) (px 4)
   (".danger-zone" |> h2) ? do
-    color "#dc2626"
+    color WebStyle.cRed
     marginBottom (px 12)
   ".back-link" ? do
     marginTop (px 24)
     paddingTop (px 16)
-    borderTop (px 1) solid "#e5e7eb"
+    borderTop (px 1) solid WebStyle.cBorderSubtle
   (".back-link" |> a) ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     textDecoration none
   (".back-link" |> a) # hover ? do
-    color "#374151"
+    color WebStyle.cFg
     textDecoration underline
   ".task-link" ? do
-    color "#0066cc"
+    color WebStyle.cAccent
     textDecoration none
     fontWeight (weight 500)
   ".task-link" # hover ? textDecoration underline
   ".error-msg" ? do
-    color "#dc2626"
-    backgroundColor "#fef2f2"
+    color WebStyle.cRed
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
     padding (px 16) (px 16) (px 16) (px 16)
     borderRadius (px 4) (px 4) (px 4) (px 4)
-    border (px 1) solid "#fecaca"
+    border (px 1) solid WebStyle.cRed
   ".create-fact-section" ? do
     marginBottom (px 16)
   ".create-fact-toggle" ? do
@@ -997,18 +1007,18 @@ formStyles = do
   ".fact-create-form" ? do
     marginTop (px 12)
     padding (px 16) (px 16) (px 16) (px 16)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 4) (px 4) (px 4) (px 4)
-    border (px 1) solid "#d1d5db"
+    border (px 1) solid WebStyle.cBorder
 
 executionDetailsStyles :: Css
 executionDetailsStyles = do
   ".execution-section" ? do
     marginTop (em 1)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
   ".execution-details" ? do
     marginTop (px 8)
   ".metric-row" ? do
@@ -1019,20 +1029,20 @@ executionDetailsStyles = do
   ".metric-label" ? do
     fontWeight (weight 600)
     width (px 120)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontSize (px 13)
   ".metric-value" ? do
     Stylesheet.key "flex" ("1" :: Text)
     fontSize (px 13)
   ".amp-link" ? do
-    color "#0066cc"
+    color WebStyle.cAccent
     textDecoration none
     wordBreak breakAll
   ".amp-link" # hover ? textDecoration underline
   ".amp-thread-btn" ? do
     display inlineBlock
     padding (px 4) (px 10) (px 4) (px 10)
-    backgroundColor "#7c3aed"
+    backgroundColor WebStyle.cPurple
     color white
     borderRadius (px 3) (px 3) (px 3) (px 3)
     textDecoration none
@@ -1040,23 +1050,23 @@ executionDetailsStyles = do
     fontWeight (weight 500)
     transition "background-color" (ms 150) ease (sec 0)
   ".amp-thread-btn" # hover ? do
-    backgroundColor "#6d28d9"
+    backgroundColor WebStyle.cPurple
     textDecoration none
   ".retry-count" ? do
-    color "#f97316"
+    color WebStyle.cOrange
     fontWeight (weight 600)
   ".attempts-divider" ? do
     margin (px 12) (px 0) (px 12) (px 0)
     border (px 0) none transparent
-    borderTop (px 1) solid "#e5e7eb"
+    borderTop (px 1) solid WebStyle.cBorderSubtle
   ".attempt-header" ? do
     fontWeight (weight 600)
     fontSize (px 13)
-    color "#374151"
+    color WebStyle.cFg
     marginTop (px 8)
     marginBottom (px 4)
     paddingBottom (px 4)
-    borderBottom (px 1) solid "#f3f4f6"
+    borderBottom (px 1) solid WebStyle.cBorderSubtle
   ".aggregated-metrics" ? do
     marginTop (em 0.5)
     paddingTop (em 0.75)
@@ -1066,21 +1076,21 @@ executionDetailsStyles = do
     Stylesheet.key "gap" ("10px" :: Text)
     marginTop (px 8)
   ".metric-card" ? do
-    backgroundColor "#f9fafb"
-    border (px 1) solid "#e5e7eb"
+    backgroundColor WebStyle.cBgRaised
+    border (px 1) solid WebStyle.cBorderSubtle
     borderRadius (px 4) (px 4) (px 4) (px 4)
     padding (px 10) (px 12) (px 10) (px 12)
     textAlign center
   (".metric-card" |> ".metric-value") ? do
     fontSize (px 20)
     fontWeight bold
-    color "#374151"
+    color WebStyle.cFg
     display block
     marginBottom (px 2)
     width auto
   (".metric-card" |> ".metric-label") ? do
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontWeight (weight 400)
     width auto
 
@@ -1088,10 +1098,10 @@ activityTimelineStyles :: Css
 activityTimelineStyles = do
   ".activity-section" ? do
     marginTop (em 1)
-    backgroundColor white
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 2) (px 2) (px 2) (px 2)
     padding (px 8) (px 10) (px 8) (px 10)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
   ".activity-timeline" ? do
     position relative
     paddingLeft (px 20)
@@ -1103,7 +1113,7 @@ activityTimelineStyles = do
     top (px 0)
     bottom (px 0)
     width (px 2)
-    backgroundColor "#e5e7eb"
+    backgroundColor WebStyle.cBorder
   ".activity-item" ? do
     position relative
     display flex
@@ -1122,8 +1132,8 @@ activityTimelineStyles = do
     justifyContent center
     fontSize (px 8)
     fontWeight bold
-    backgroundColor white
-    border (px 2) solid "#e5e7eb"
+    backgroundColor WebStyle.cBgRaised
+    border (px 2) solid WebStyle.cBorder
   ".activity-content" ? do
     Stylesheet.key "flex" ("1" :: Text)
   ".activity-header" ? do
@@ -1136,43 +1146,43 @@ activityTimelineStyles = do
     fontSize (px 12)
   ".activity-time" ? do
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".activity-message" ? do
     margin (px 2) (px 0) (px 0) (px 0)
     fontSize (px 12)
-    color "#374151"
+    color WebStyle.cFg
   ".activity-metadata" ? do
     marginTop (px 4)
   (".activity-metadata" |> "summary") ? do
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     cursor pointer
   ".metadata-json" ? do
     fontSize (px 10)
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
     padding (px 4) (px 6) (px 4) (px 6)
     borderRadius (px 2) (px 2) (px 2) (px 2)
     marginTop (px 2)
     maxHeight (px 150)
     overflow auto
   ".stage-claiming" |> ".activity-icon" ? do
-    borderColor "#3b82f6"
-    color "#3b82f6"
+    borderColor WebStyle.cBlue
+    color WebStyle.cBlue
   ".stage-running" |> ".activity-icon" ? do
-    borderColor "#f59e0b"
-    color "#f59e0b"
+    borderColor WebStyle.cYellow
+    color WebStyle.cYellow
   ".stage-reviewing" |> ".activity-icon" ? do
-    borderColor "#8b5cf6"
-    color "#8b5cf6"
+    borderColor WebStyle.cPurple
+    color WebStyle.cPurple
   ".stage-retrying" |> ".activity-icon" ? do
-    borderColor "#f97316"
-    color "#f97316"
+    borderColor WebStyle.cOrange
+    color WebStyle.cOrange
   ".stage-completed" |> ".activity-icon" ? do
-    borderColor "#10b981"
-    color "#10b981"
+    borderColor WebStyle.cGreen
+    color WebStyle.cGreen
   ".stage-failed" |> ".activity-icon" ? do
-    borderColor "#ef4444"
-    color "#ef4444"
+    borderColor WebStyle.cRed
+    color WebStyle.cRed
 
 commitStyles :: Css
 commitStyles = do
@@ -1183,9 +1193,9 @@ commitStyles = do
     marginTop (px 8)
   ".commit-item" ? do
     padding (px 6) (px 8) (px 6) (px 8)
-    backgroundColor "#f9fafb"
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 2) (px 2) (px 2) (px 2)
-    border (px 1) solid "#e5e7eb"
+    border (px 1) solid WebStyle.cBorderSubtle
   ".commit-header" ? do
     display flex
     alignItems center
@@ -1194,24 +1204,24 @@ commitStyles = do
   ".commit-hash" ? do
     fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
     fontSize (px 12)
-    color "#0066cc"
+    color WebStyle.cAccent
     textDecoration none
-    backgroundColor "#e5e7eb"
+    backgroundColor WebStyle.cBorder
     padding (px 1) (px 4) (px 1) (px 4)
     borderRadius (px 2) (px 2) (px 2) (px 2)
   ".commit-hash" # hover ? textDecoration underline
   ".commit-summary" ? do
     fontSize (px 13)
-    color "#374151"
+    color WebStyle.cFg
     fontWeight (weight 500)
   ".commit-meta" ? do
     display flex
     Stylesheet.key "gap" ("12px" :: Text)
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".commit-author" ? fontWeight (weight 500)
   ".commit-files" ? do
-    color "#9ca3af"
+    color WebStyle.cFgFaint
 
 markdownStyles :: Css
 markdownStyles = do
@@ -1219,13 +1229,13 @@ markdownStyles = do
     width (pct 100)
     lineHeight (em 1.6)
     fontSize (px 14)
-    color "#374151"
+    color WebStyle.cFg
   ".md-h1" ? do
     fontSize (px 18)
     fontWeight bold
     margin (em 1) (px 0) (em 0.5) (px 0)
     paddingBottom (em 0.3)
-    borderBottom (px 1) solid "#e5e7eb"
+    borderBottom (px 1) solid WebStyle.cBorderSubtle
   ".md-h2" ? do
     fontSize (px 16)
     fontWeight (weight 600)
@@ -1239,11 +1249,11 @@ markdownStyles = do
   ".md-code" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 12)
-    backgroundColor "#f8f8f8"
-    color "#333333"
+    backgroundColor WebStyle.cBgRaised
+    color WebStyle.cFg
     padding (px 10) (px 12) (px 10) (px 12)
     borderRadius (px 4) (px 4) (px 4) (px 4)
-    border (px 1) solid "#e1e4e8"
+    border (px 1) solid WebStyle.cBorder
     overflow auto
     whiteSpace preWrap
     margin (em 0.5) (px 0) (em 0.5) (px 0)
@@ -1255,7 +1265,7 @@ markdownStyles = do
   ".md-inline-code" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (em 0.9)
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
     padding (px 1) (px 4) (px 1) (px 4)
     borderRadius (px 2) (px 2) (px 2) (px 2)
 
@@ -1266,11 +1276,11 @@ retryBannerStyles = do
     padding (px 12) (px 16) (px 12) (px 16)
     margin (px 0) (px 0) (px 16) (px 0)
   ".retry-banner-warning" ? do
-    backgroundColor "#fef3c7"
-    border (px 1) solid "#f59e0b"
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    border (px 1) solid WebStyle.cYellow
   ".retry-banner-critical" ? do
-    backgroundColor "#fee2e2"
-    border (px 1) solid "#ef4444"
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
+    border (px 1) solid WebStyle.cRed
   ".retry-banner-header" ? do
     display flex
     alignItems center
@@ -1282,9 +1292,9 @@ retryBannerStyles = do
   ".retry-attempt" ? do
     fontSize (px 14)
     fontWeight (weight 600)
-    color "#374151"
+    color WebStyle.cFg
   ".retry-warning-badge" ? do
-    backgroundColor "#dc2626"
+    backgroundColor WebStyle.cRed
     color white
     fontSize (px 11)
     fontWeight (weight 600)
@@ -1293,7 +1303,7 @@ retryBannerStyles = do
     marginLeft auto
   ".retry-banner-details" ? do
     fontSize (px 13)
-    color "#374151"
+    color WebStyle.cFg
   ".retry-detail-row" ? do
     display flex
     alignItems flexStart
@@ -1304,11 +1314,11 @@ retryBannerStyles = do
     minWidth (px 110)
     flexShrink 0
   ".retry-value" ? do
-    color "#4b5563"
+    color WebStyle.cFgMuted
   ".retry-commit" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (em 0.9)
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
     padding (px 1) (px 4) (px 1) (px 4)
     borderRadius (px 2) (px 2) (px 2) (px 2)
   ".retry-conflict-list" ? do
@@ -1321,15 +1331,15 @@ retryBannerStyles = do
   ".retry-warning-message" ? do
     marginTop (px 12)
     padding (px 10) (px 12) (px 10) (px 12)
-    backgroundColor "#fecaca"
+    Stylesheet.key "background" ("rgba(248,113,113,0.2)" :: Text)
     borderRadius (px 2) (px 2) (px 2) (px 2)
     fontSize (px 12)
-    color "#991b1b"
+    color WebStyle.cRed
     fontWeight (weight 500)
   ".retry-hint" ? do
     marginTop (px 8)
     fontSize (px 12)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontStyle italic
 
 commentStyles :: Css
@@ -1337,15 +1347,15 @@ commentStyles = do
   ".comments-section" ? do
     marginTop (px 12)
   ".comment-card" ? do
-    backgroundColor "#f9fafb"
-    border (px 1) solid "#e5e7eb"
+    backgroundColor WebStyle.cBgRaised
+    border (px 1) solid WebStyle.cBorderSubtle
     borderRadius (px 4) (px 4) (px 4) (px 4)
     padding (px 10) (px 12) (px 10) (px 12)
     marginBottom (px 8)
   ".comment-text" ? do
     margin (px 0) (px 0) (px 6) (px 0)
     fontSize (px 13)
-    color "#374151"
+    color WebStyle.cFg
     whiteSpace preWrap
   ".comment-meta" ? do
     display flex
@@ -1360,14 +1370,14 @@ commentStyles = do
     textTransform uppercase
     whiteSpace nowrap
   ".author-human" ? do
-    backgroundColor "#dbeafe"
-    color "#1e40af"
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+    color WebStyle.cBlue
   ".author-junior" ? do
-    backgroundColor "#d1fae5"
-    color "#065f46"
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+    color WebStyle.cGreen
   ".comment-time" ? do
     fontSize (px 11)
-    color "#9ca3af"
+    color WebStyle.cFgFaint
   ".comment-form" ? do
     marginTop (px 12)
     display flex
@@ -1377,13 +1387,13 @@ commentStyles = do
     width (pct 100)
     padding (px 8) (px 10) (px 8) (px 10)
     fontSize (px 13)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
     borderRadius (px 4) (px 4) (px 4) (px 4)
     Stylesheet.key "resize" ("vertical" :: Text)
     minHeight (px 60)
   ".comment-textarea" # focus ? do
     Stylesheet.key "outline" ("none" :: Text)
-    borderColor "#0066cc"
+    borderColor WebStyle.cAccentDim
 
 timeFilterStyles :: Css
 timeFilterStyles = do
@@ -1399,22 +1409,22 @@ timeFilterStyles = do
     fontWeight (weight 500)
     textDecoration none
     borderRadius (px 12) (px 12) (px 12) (px 12)
-    border (px 1) solid "#d0d0d0"
-    backgroundColor white
-    color "#374151"
+    border (px 1) solid WebStyle.cBorder
+    backgroundColor WebStyle.cBgRaised
+    color WebStyle.cFg
     transition "all" (ms 150) ease (sec 0)
     cursor pointer
   ".time-filter-btn" # hover ? do
-    borderColor "#999"
-    backgroundColor "#f3f4f6"
+    borderColor WebStyle.cFgFaint
+    backgroundColor WebStyle.cBgHover
     textDecoration none
   ".time-filter-btn.active" ? do
-    backgroundColor "#0066cc"
-    borderColor "#0066cc"
+    backgroundColor WebStyle.cAccentDim
+    borderColor WebStyle.cAccentDim
     color white
   ".time-filter-btn.active" # hover ? do
-    backgroundColor "#0055aa"
-    borderColor "#0055aa"
+    backgroundColor WebStyle.cAccent
+    borderColor WebStyle.cAccent
 
 sortDropdownStyles :: Css
 sortDropdownStyles = do
@@ -1433,7 +1443,7 @@ sortDropdownStyles = do
     Stylesheet.key "gap" ("6px" :: Text)
     fontSize (px 13)
   ".sort-label" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontWeight (weight 500)
   ".sort-dropdown-wrapper" ? do
     position relative
@@ -1441,16 +1451,16 @@ sortDropdownStyles = do
     padding (px 4) (px 10) (px 4) (px 10)
     fontSize (px 13)
     fontWeight (weight 500)
-    border (px 1) solid "#d0d0d0"
+    border (px 1) solid WebStyle.cBorder
     borderRadius (px 4) (px 4) (px 4) (px 4)
-    backgroundColor white
-    color "#374151"
+    backgroundColor WebStyle.cBgRaised
+    color WebStyle.cFg
     cursor pointer
     transition "all" (ms 150) ease (sec 0)
     whiteSpace nowrap
   ".sort-dropdown-btn" # hover ? do
-    borderColor "#999"
-    backgroundColor "#f3f4f6"
+    borderColor WebStyle.cFgFaint
+    backgroundColor WebStyle.cBgHover
   ".sort-dropdown-content" ? do
     minWidth (px 160)
     right (px 0)
@@ -1459,7 +1469,7 @@ sortDropdownStyles = do
     padding (px 8) (px 12) (px 8) (px 12)
     fontSize (px 13)
   ".sort-dropdown-item.active" ? do
-    backgroundColor "#e0f2fe"
+    Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
     fontWeight (weight 600)
 
 taskMetaStyles :: Css
@@ -1479,17 +1489,17 @@ taskMetaStyles = do
     flexWrap Flexbox.wrap
     Stylesheet.key "gap" ("6px" :: Text)
     fontSize (px 12)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".task-meta-id" ? do
     fontFamily ["SF Mono", "Monaco", "monospace"] [monospace]
     fontSize (px 13)
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
     padding (px 1) (px 4) (px 1) (px 4)
     borderRadius (px 2) (px 2) (px 2) (px 2)
   ".task-meta-label" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".meta-sep" ? do
-    color "#d1d5db"
+    color WebStyle.cFg
     Stylesheet.key "user-select" ("none" :: Text)
 
 timelineEventStyles :: Css
@@ -1505,20 +1515,20 @@ timelineEventStyles = do
     textAlign center
   ".event-label" ? do
     fontWeight (weight 500)
-    color "#374151"
+    color WebStyle.cFg
   ".event-assistant" ? do
     padding (px 0) (px 0) (px 0) (px 0)
   ".event-bubble" ? do
-    backgroundColor "#f3f4f6"
+    backgroundColor WebStyle.cBgHover
     padding (px 8) (px 12) (px 8) (px 12)
     borderRadius (px 8) (px 8) (px 8) (px 8)
     whiteSpace preWrap
     lineHeight (em 1.5)
   ".event-truncated" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontStyle italic
   ".event-tool-call" ? do
-    borderLeft (px 3) solid "#3b82f6"
+    borderLeft (px 3) solid WebStyle.cBlue
     paddingLeft (px 8)
   ".event-tool-call" |> "summary" ? do
     cursor pointer
@@ -1529,17 +1539,17 @@ timelineEventStyles = do
   ".event-tool-call" |> "summary" # before ? do
     content (stringContent "▶")
     fontSize (px 10)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     transition "transform" (ms 150) ease (sec 0)
   ".event-tool-call[open]" |> "summary" # before ? do
     Stylesheet.key "transform" ("rotate(90deg)" :: Text)
   ".tool-name" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
-    color "#3b82f6"
+    color WebStyle.cBlue
   ".tool-summary" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 12)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     marginLeft (px 8)
   ".tool-args" ? do
     marginTop (px 4)
@@ -1547,8 +1557,8 @@ timelineEventStyles = do
   ".tool-output-pre" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 11)
-    backgroundColor "#1e1e1e"
-    color "#d4d4d4"
+    backgroundColor WebStyle.cBg
+    color WebStyle.cFg
     padding (px 8) (px 10) (px 8) (px 10)
     borderRadius (px 4) (px 4) (px 4) (px 4)
     overflowX auto
@@ -1556,27 +1566,27 @@ timelineEventStyles = do
     maxHeight (px 300)
     margin (px 0) (px 0) (px 0) (px 0)
   ".event-tool-result" ? do
-    borderLeft (px 3) solid "#10b981"
+    borderLeft (px 3) solid WebStyle.cGreen
     paddingLeft (px 8)
   ".result-header" ? do
     fontSize (px 12)
   ".line-count" ? do
     fontSize (px 11)
-    color "#6b7280"
-    backgroundColor "#f3f4f6"
+    color WebStyle.cFgMuted
+    backgroundColor WebStyle.cBgHover
     padding (px 1) (px 6) (px 1) (px 6)
     borderRadius (px 10) (px 10) (px 10) (px 10)
   ".result-collapsible" |> "summary" ? do
     cursor pointer
     fontSize (px 12)
-    color "#0066cc"
+    color WebStyle.cAccent
     marginBottom (px 4)
   ".result-collapsible" |> "summary" # hover ? textDecoration underline
   ".tool-output" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 11)
-    backgroundColor "#1e1e1e"
-    color "#d4d4d4"
+    backgroundColor WebStyle.cBg
+    color WebStyle.cFg
     padding (px 8) (px 10) (px 8) (px 10)
     borderRadius (px 4) (px 4) (px 4) (px 4)
     overflowX auto
@@ -1588,19 +1598,19 @@ timelineEventStyles = do
     alignItems center
     Stylesheet.key "gap" ("6px" :: Text)
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     padding (px 4) (px 0) (px 4) (px 0)
   ".cost-text" ? do
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
   ".event-error" ? do
-    borderLeft (px 3) solid "#ef4444"
+    borderLeft (px 3) solid WebStyle.cRed
     paddingLeft (px 8)
-    backgroundColor "#fef2f2"
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
     padding (px 8) (px 8) (px 8) (px 12)
     borderRadius (px 4) (px 4) (px 4) (px 4)
-  ".event-error" |> ".event-label" ? color "#dc2626"
+  ".event-error" |> ".event-label" ? color WebStyle.cRed
   ".error-message" ? do
-    color "#dc2626"
+    color WebStyle.cRed
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 12)
     whiteSpace preWrap
@@ -1608,13 +1618,13 @@ timelineEventStyles = do
     display flex
     alignItems center
     Stylesheet.key "gap" ("8px" :: Text)
-    color "#10b981"
+    color WebStyle.cGreen
     fontWeight (weight 500)
     padding (px 8) (px 0) (px 8) (px 0)
   ".output-collapsible" |> "summary" ? do
     cursor pointer
     fontSize (px 12)
-    color "#0066cc"
+    color WebStyle.cAccent
     marginBottom (px 4)
   ".output-collapsible" |> "summary" # hover ? textDecoration underline
   Stylesheet.key "@keyframes pulse" ("0%, 100% { opacity: 1; } 50% { opacity: 0.5; }" :: Text)
@@ -1624,49 +1634,49 @@ unifiedTimelineStyles = do
   ".unified-timeline-section" ? do
     marginTop (em 1.5)
     paddingTop (em 1)
-    borderTop (px 1) solid "#e5e7eb"
+    borderTop (px 1) solid WebStyle.cBorderSubtle
   ".timeline-live-toggle" ? do
     fontSize (px 10)
     fontWeight bold
-    color "#10b981"
-    backgroundColor "#d1fae5"
+    color WebStyle.cGreen
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
     padding (px 2) (px 6) (px 2) (px 6)
     borderRadius (px 10) (px 10) (px 10) (px 10)
     marginLeft (px 8)
     textTransform uppercase
-    border (px 1) solid "#6ee7b7"
+    border (px 1) solid WebStyle.cGreen
     cursor pointer
     Stylesheet.key "transition" ("all 0.3s ease" :: Text)
     Stylesheet.key "animation" ("pulse 2s infinite" :: Text)
   ".timeline-live-toggle:hover" ? do
     Stylesheet.key "box-shadow" ("0 0 8px rgba(16,185,129,0.4)" :: Text)
   ".timeline-live-toggle.timeline-live-paused" ? do
-    color "#6b7280"
-    backgroundColor "#f3f4f6"
-    border (px 1) solid "#d1d5db"
+    color WebStyle.cFgMuted
+    backgroundColor WebStyle.cBgHover
+    border (px 1) solid WebStyle.cBorder
     Stylesheet.key "animation" ("none" :: Text)
   ".timeline-autoscroll-toggle" ? do
     fontSize (px 10)
     fontWeight bold
-    color "#3b82f6"
-    backgroundColor "#dbeafe"
+    color WebStyle.cBlue
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
     padding (px 2) (px 6) (px 2) (px 6)
     borderRadius (px 10) (px 10) (px 10) (px 10)
     marginLeft (px 4)
-    border (px 1) solid "#93c5fd"
+    border (px 1) solid WebStyle.cBlue
     cursor pointer
     Stylesheet.key "transition" ("all 0.2s ease" :: Text)
   ".timeline-autoscroll-toggle:hover" ? do
     Stylesheet.key "box-shadow" ("0 0 6px rgba(59,130,246,0.3)" :: Text)
   ".timeline-autoscroll-toggle.timeline-autoscroll-disabled" ? do
-    color "#6b7280"
-    backgroundColor "#f3f4f6"
-    border (px 1) solid "#d1d5db"
+    color WebStyle.cFgMuted
+    backgroundColor WebStyle.cBgHover
+    border (px 1) solid WebStyle.cBorder
   ".timeline-live" ? do
     fontSize (px 10)
     fontWeight bold
-    color "#10b981"
-    backgroundColor "#d1fae5"
+    color WebStyle.cGreen
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
     padding (px 2) (px 6) (px 2) (px 6)
     borderRadius (px 10) (px 10) (px 10) (px 10)
     marginLeft (px 8)
@@ -1690,19 +1700,19 @@ unifiedTimelineStyles = do
     marginLeft (px 4)
     marginRight (px 4)
   ".actor-human" ? do
-    color "#7c3aed"
-    backgroundColor "#f3e8ff"
+    color WebStyle.cPurple
+    Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
   ".actor-junior" ? do
-    color "#0369a1"
-    backgroundColor "#e0f2fe"
+    color WebStyle.cBlue
+    Stylesheet.key "background" ("rgba(96,165,250,0.1)" :: Text)
   ".actor-system" ? do
-    color "#6b7280"
-    backgroundColor "#f3f4f6"
+    color WebStyle.cFgMuted
+    backgroundColor WebStyle.cBgHover
   ".timeline-comment" ? do
     paddingLeft (px 4)
   ".timeline-comment" |> ".comment-bubble" ? do
-    backgroundColor "#f3f4f6"
-    color "#1f2937"
+    backgroundColor WebStyle.cBgHover
+    color WebStyle.cFg
     padding (px 10) (px 14) (px 10) (px 14)
     borderRadius (px 8) (px 8) (px 8) (px 8)
     whiteSpace preWrap
@@ -1713,39 +1723,39 @@ unifiedTimelineStyles = do
     Stylesheet.key "gap" ("6px" :: Text)
     flexWrap Flexbox.wrap
     padding (px 6) (px 8) (px 6) (px 8)
-    backgroundColor "#f0fdf4"
+    Stylesheet.key "background" ("rgba(74,222,128,0.1)" :: Text)
     borderRadius (px 6) (px 6) (px 6) (px 6)
-    borderLeft (px 3) solid "#22c55e"
+    borderLeft (px 3) solid WebStyle.cGreen
   ".status-change-text" ? do
     fontWeight (weight 500)
-    color "#166534"
+    color WebStyle.cGreen
   ".timeline-activity" ? do
     display flex
     alignItems center
     Stylesheet.key "gap" ("6px" :: Text)
     flexWrap Flexbox.wrap
     padding (px 4) (px 0) (px 4) (px 0)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".activity-detail" ? do
     fontSize (px 11)
-    color "#9ca3af"
+    color WebStyle.cFgFaint
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
   ".timeline-error" ? do
-    borderLeft (px 3) solid "#ef4444"
-    backgroundColor "#fef2f2"
+    borderLeft (px 3) solid WebStyle.cRed
+    Stylesheet.key "background" ("rgba(248,113,113,0.1)" :: Text)
     padding (px 8) (px 12) (px 8) (px 12)
     borderRadius (px 4) (px 4) (px 4) (px 4)
   ".timeline-error" |> ".error-message" ? do
     marginTop (px 6)
-    color "#dc2626"
+    color WebStyle.cRed
     fontFamily ["SF Mono", "Monaco", "Consolas", "monospace"] [monospace]
     fontSize (px 12)
     whiteSpace preWrap
   ".timeline-thought" ? do
     paddingLeft (px 4)
   ".timeline-thought" |> ".thought-bubble" ? do
-    backgroundColor "#fef3c7"
-    color "#78350f"
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    color WebStyle.cYellow
     padding (px 8) (px 12) (px 8) (px 12)
     borderRadius (px 8) (px 8) (px 8) (px 8)
     whiteSpace preWrap
@@ -1753,7 +1763,7 @@ unifiedTimelineStyles = do
     fontSize (px 12)
     lineHeight (em 1.5)
   ".timeline-tool-call" ? do
-    borderLeft (px 3) solid "#3b82f6"
+    borderLeft (px 3) solid WebStyle.cBlue
     paddingLeft (px 8)
   ".timeline-tool-call" |> "summary" ? do
     cursor pointer
@@ -1764,12 +1774,12 @@ unifiedTimelineStyles = do
   ".timeline-tool-call" |> "summary" # before ? do
     content (stringContent "▶")
     fontSize (px 10)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     transition "transform" (ms 150) ease (sec 0)
   ".timeline-tool-call[open]" |> "summary" # before ? do
     Stylesheet.key "transform" ("rotate(90deg)" :: Text)
   ".timeline-tool-result" ? do
-    borderLeft (px 3) solid "#10b981"
+    borderLeft (px 3) solid WebStyle.cGreen
     paddingLeft (px 8)
   ".timeline-tool-result" |> "summary" ? do
     cursor pointer
@@ -1782,11 +1792,11 @@ unifiedTimelineStyles = do
     alignItems center
     Stylesheet.key "gap" ("6px" :: Text)
     fontSize (px 11)
-    color "#6b7280"
+    color WebStyle.cFgMuted
     padding (px 2) (px 0) (px 2) (px 0)
   ".timeline-checkpoint" ? do
-    borderLeft (px 3) solid "#8b5cf6"
-    backgroundColor "#faf5ff"
+    borderLeft (px 3) solid WebStyle.cPurple
+    Stylesheet.key "background" ("rgba(167,139,250,0.1)" :: Text)
     padding (px 8) (px 12) (px 8) (px 12)
     borderRadius (px 4) (px 4) (px 4) (px 4)
   ".timeline-checkpoint" |> ".checkpoint-content" ? do
@@ -1794,21 +1804,21 @@ unifiedTimelineStyles = do
     fontSize (px 12)
     whiteSpace preWrap
   ".timeline-guardrail" ? do
-    borderLeft (px 3) solid "#f59e0b"
-    backgroundColor "#fffbeb"
+    borderLeft (px 3) solid WebStyle.cYellow
+    Stylesheet.key "background" ("rgba(250,204,21,0.1)" :: Text)
     padding (px 8) (px 12) (px 8) (px 12)
     borderRadius (px 4) (px 4) (px 4) (px 4)
   ".timeline-guardrail" |> ".guardrail-content" ? do
     marginTop (px 6)
     fontSize (px 12)
-    color "#92400e"
+    color WebStyle.cYellow
   ".timeline-generic" ? do
     padding (px 4) (px 0) (px 4) (px 0)
-    color "#6b7280"
+    color WebStyle.cFgMuted
   ".formatted-json" ? do
     margin (px 0) (px 0) (px 0) (px 0)
     padding (px 8) (px 8) (px 8) (px 8)
-    backgroundColor "#f9fafb"
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 4) (px 4) (px 4) (px 4)
     overflowX auto
     fontSize (px 12)
@@ -1827,20 +1837,20 @@ compactToolStyles = do
     fontSize (px 12)
     padding (px 2) (px 0) (px 2) (px 0)
   ".tool-check" ? do
-    color "#10b981"
+    color WebStyle.cGreen
     fontWeight bold
   ".tool-label" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontWeight (weight 500)
   ".tool-path" ? do
-    color "#3b82f6"
+    color WebStyle.cBlue
   ".tool-pattern" ? do
-    color "#8b5cf6"
-    backgroundColor "#f5f3ff"
+    color WebStyle.cPurple
+    Stylesheet.key "background" ("rgba(167,139,250,0.1)" :: Text)
     padding (px 1) (px 4) (px 1) (px 4)
     borderRadius (px 2) (px 2) (px 2) (px 2)
   ".tool-path-suffix" ? do
-    color "#6b7280"
+    color WebStyle.cFgMuted
     fontSize (px 11)
   ".tool-bash" ? do
     display flex
@@ -1850,12 +1860,12 @@ compactToolStyles = do
     fontSize (px 12)
     padding (px 2) (px 0) (px 2) (px 0)
   ".tool-bash-prompt" ? do
-    color "#f59e0b"
+    color WebStyle.cYellow
     fontWeight bold
     fontSize (px 14)
   ".tool-bash-cmd" ? do
-    color "#374151"
-    backgroundColor "#f3f4f6"
+    color WebStyle.cFg
+    backgroundColor WebStyle.cBgHover
     padding (px 2) (px 6) (px 2) (px 6)
     borderRadius (px 3) (px 3) (px 3) (px 3)
     wordBreak breakAll
@@ -1870,7 +1880,7 @@ compactToolStyles = do
   ".tool-args-pre" ? do
     margin (px 4) (px 0) (px 0) (px 16)
     padding (px 6) (px 8) (px 6) (px 8)
-    backgroundColor "#f9fafb"
+    backgroundColor WebStyle.cBgRaised
     borderRadius (px 3) (px 3) (px 3) (px 3)
     fontSize (px 11)
     whiteSpace preWrap
@@ -1964,302 +1974,11 @@ responsiveStyles = do
       justifyContent spaceBetween
       marginTop (px 4)
 
+-- | Dark mode styles are now handled by the base styles being dark-first.
+-- The shared Omni.Web.Style design system uses dark-first tokens, so this
+-- function is now a no-op. Kept for structural consistency.
 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"
-      <> ".list-group"
-      ? do
-        backgroundColor "#1f2937"
-        borderColor "#374151"
-    ".list-group-item" ? borderBottomColor "#374151"
-    ".list-group-item" # hover ? backgroundColor "#374151"
-    ".list-group-item-id" ? color "#60a5fa"
-    ".list-group-item-title" ? color "#d1d5db"
-    header ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-    ".navbar" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-    ".navbar-brand" ? color "#60a5fa"
-    ".navbar-link" ? color "#d1d5db"
-    ".navbar-link" # hover ? backgroundColor "#374151"
-    ".navbar-dropdown-btn" ? color "#d1d5db"
-    ".navbar-dropdown-btn" # hover ? backgroundColor "#374151"
-    ".navbar-dropdown-content" ? do
-      backgroundColor "#1f2937"
-      Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
-    ".navbar-dropdown-item" ? color "#d1d5db"
-    ".navbar-dropdown-item" # hover ? backgroundColor "#374151"
-    ".status-dropdown-menu" ? do
-      backgroundColor "#1f2937"
-      Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
-    ".hamburger-line" ? backgroundColor "#d1d5db"
-    ".nav-brand" ? color "#f3f4f6"
-    "h2" <> "h3" ? color "#d1d5db"
-    a ? color "#60a5fa"
-    ".breadcrumb-container" ? backgroundColor transparent
-    ".breadcrumb-sep" ? color "#6b7280"
-    ".breadcrumb-current" ? color "#9ca3af"
-
-    ".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"
-    ".task-meta-id" ? do
-      backgroundColor "#374151"
-      color "#e5e7eb"
-    ".task-meta-secondary" ? color "#9ca3af"
-    ".meta-sep" ? color "#4b5563"
-    ".task-meta-label" ? color "#9ca3af"
-    ".detail-section" ? borderTopColor "#374151"
-    ".description" ? do
-      backgroundColor "#374151"
-      color "#e5e7eb"
-    ".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"
-    ".badge-needshelp" ? do
-      backgroundColor "#78350f"
-      color "#fcd34d"
-    ".badge-p0" ? do
-      backgroundColor "#7f1d1d"
-      color "#fca5a5"
-    ".badge-p1" ? do
-      backgroundColor "#78350f"
-      color "#fcd34d"
-    ".badge-p2" ? do
-      backgroundColor "#1e3a8a"
-      color "#93c5fd"
-    ".badge-p3" ? do
-      backgroundColor "#374151"
-      color "#d1d5db"
-    ".badge-p4" ? do
-      backgroundColor "#1f2937"
-      color "#9ca3af"
-    ".blocking-impact" ? do
-      backgroundColor "#374151"
-      color "#9ca3af"
-    ".priority-dropdown-menu" ? do
-      backgroundColor "#1f2937"
-      Stylesheet.key "box-shadow" ("0 2px 8px rgba(0,0,0,0.3)" :: Text)
-    ".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"
-    ".stats-section" <> ".summary-section" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-
-    (".stat-card.badge-open" |> ".stat-count") ? color "#fbbf24"
-    (".stat-card.badge-inprogress" |> ".stat-count") ? color "#60a5fa"
-    (".stat-card.badge-review" |> ".stat-count") ? color "#a78bfa"
-    (".stat-card.badge-approved" |> ".stat-count") ? color "#22d3ee"
-    (".stat-card.badge-done" |> ".stat-count") ? color "#34d399"
-    (".stat-card.badge-neutral" |> ".stat-count") ? color "#9ca3af"
-
-    ".progress-bar" ? backgroundColor "#374151"
-    ".progress-fill" ? backgroundColor "#60a5fa"
-    ".multi-progress-bar" ? backgroundColor "#374151"
-    ".progress-legend" ? color "#9ca3af"
-    ".activity-section" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-    ".activity-timeline" # before ? backgroundColor "#374151"
-    ".activity-icon" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-    ".activity-time" ? color "#9ca3af"
-    ".activity-message" ? color "#d1d5db"
-    (".activity-metadata" |> "summary") ? color "#9ca3af"
-    ".metadata-json" ? backgroundColor "#374151"
-    ".execution-section" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-
-    ".metric-label" ? color "#9ca3af"
-    ".metric-value" ? color "#d1d5db"
-    ".metric-card" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-    (".metric-card" |> ".metric-value") ? color "#f3f4f6"
-    (".metric-card" |> ".metric-label") ? color "#9ca3af"
-    ".amp-link" ? color "#60a5fa"
-    ".amp-thread-btn" ? do
-      backgroundColor "#8b5cf6"
-    ".amp-thread-btn" # hover ? backgroundColor "#7c3aed"
-    ".markdown-content" ? color "#d1d5db"
-    ".commit-item" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-    ".commit-hash" ? do
-      backgroundColor "#4b5563"
-      color "#60a5fa"
-    ".commit-summary" ? color "#d1d5db"
-    ".commit-meta" ? color "#9ca3af"
-    ".md-h1" ? borderBottomColor "#374151"
-    ".md-code" ? do
-      backgroundColor "#1e1e1e"
-      color "#d4d4d4"
-      borderColor "#374151"
-    ".md-inline-code" ? do
-      backgroundColor "#374151"
-      color "#f3f4f6"
-    ".edit-description" ? borderTopColor "#374151"
-    (".edit-description" |> "summary") ? color "#60a5fa"
-    ".edit-link" ? color "#60a5fa"
-    "button.cancel-link" ? do
-      color "#f87171"
-      backgroundColor transparent
-      border (px 0) solid transparent
-    ".description-textarea" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-      color "#f3f4f6"
-    ".fact-create-form" ? do
-      backgroundColor "#1f2937"
-      borderColor "#374151"
-    ".time-filter-btn" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-      color "#d1d5db"
-    ".time-filter-btn" # hover ? do
-      backgroundColor "#4b5563"
-      borderColor "#6b7280"
-    ".time-filter-btn.active" ? do
-      backgroundColor "#3b82f6"
-      borderColor "#3b82f6"
-      color white
-    ".time-filter-btn.active" # hover ? do
-      backgroundColor "#2563eb"
-      borderColor "#2563eb"
-    ".sort-label" ? color "#9ca3af"
-    ".sort-dropdown-btn" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-      color "#d1d5db"
-    ".sort-dropdown-btn" # hover ? do
-      backgroundColor "#4b5563"
-      borderColor "#6b7280"
-    ".sort-dropdown-item.active" ? do
-      backgroundColor "#1e3a5f"
-    ".comment-card" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-    ".comment-text" ? color "#d1d5db"
-    ".author-human" ? do
-      backgroundColor "#1e3a8a"
-      color "#93c5fd"
-    ".author-junior" ? do
-      backgroundColor "#064e3b"
-      color "#6ee7b7"
-    ".comment-time" ? color "#9ca3af"
-    ".comment-textarea" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-      color "#f3f4f6"
-    ".form-input" <> ".form-textarea" ? do
-      backgroundColor "#374151"
-      borderColor "#4b5563"
-      color "#f3f4f6"
-    (".form-group" |> label) ? color "#d1d5db"
-    ".danger-zone" ? do
-      backgroundColor "#450a0a"
-      borderColor "#991b1b"
-    (".danger-zone" |> h2) ? color "#f87171"
-    ".retry-banner-warning" ? do
-      backgroundColor "#451a03"
-      borderColor "#b45309"
-    ".retry-banner-critical" ? do
-      backgroundColor "#450a0a"
-      borderColor "#dc2626"
-    ".retry-attempt" ? color "#d1d5db"
-    ".retry-banner-details" ? color "#d1d5db"
-    ".retry-value" ? color "#9ca3af"
-    ".retry-commit" ? backgroundColor "#374151"
-    ".event-bubble" ? backgroundColor "#374151"
-    ".comment-bubble" ? do
-      backgroundColor "#374151"
-      color "#d1d5db"
-    ".thought-bubble" ? do
-      backgroundColor "#292524"
-      color "#a8a29e"
-      borderRadius (px 2) (px 2) (px 2) (px 2)
-    ".event-label" ? color "#d1d5db"
-    ".tool-bash-cmd" ? do
-      backgroundColor "#292524"
-      color "#a8a29e"
-    ".tool-label" ? color "#9ca3af"
-    ".tool-path" ? color "#60a5fa"
-    ".tool-pattern" ? do
-      backgroundColor "#3b2f5e"
-      color "#c4b5fd"
-    ".output-collapsible" |> "summary" ? color "#60a5fa"
-    ".timeline-tool-call" |> "summary" # before ? color "#9ca3af"
-    ".line-count" ? do
-      backgroundColor "#374151"
-      color "#9ca3af"
-    ".event-error" ? do
-      backgroundColor "#450a0a"
-      borderColor "#dc2626"
-    ".event-error" |> ".event-label" ? color "#f87171"
-    ".error-message" ? color "#f87171"
-    ".timeline-error" |> ".event-label" ? color "#fca5a5"
-    ".timeline-guardrail" |> ".event-label" ? color "#fbbf24"
-    ".timeline-guardrail" ? do
-      backgroundColor "#451a03"
-      borderColor "#f59e0b"
-    ".timeline-guardrail" |> ".guardrail-content" ? color "#fcd34d"
-    ".formatted-json" ? do
-      backgroundColor "#1e1e1e"
-      color "#d4d4d4"
-    -- Responsive dark mode: dropdown content needs background on mobile
-    query Media.screen [Media.maxWidth (px 600)] <| do
-      ".navbar-dropdown-content" ? do
-        backgroundColor "#1f2937"
-      ".navbar-dropdown-item" # hover ? do
-        backgroundColor "#374151"
-
-prefersDark :: Stylesheet.Feature
-prefersDark =
-  Stylesheet.Feature "prefers-color-scheme" (Just (Clay.value ("dark" :: Text)))
+darkModeStyles = pure ()
 
 statusBadgeClass :: Text -> Text
 statusBadgeClass status = case status of
diff --git a/Omni/Web.hs b/Omni/Web/Core.hs
similarity index 99%
rename from Omni/Web.hs
rename to Omni/Web/Core.hs
index 6e4d19c7..99f4a37c 100644
--- a/Omni/Web.hs
+++ b/Omni/Web/Core.hs
@@ -14,7 +14,7 @@
 -- : dep sqlite-simple
 -- : dep wai
 -- : dep warp
-module Omni.Web
+module Omni.Web.Core
   ( startWebServer,
     defaultPort,
     app,
diff --git a/Omni/Web/Style.hs b/Omni/Web/Style.hs
new file mode 100644
index 00000000..21050e93
--- /dev/null
+++ b/Omni/Web/Style.hs
@@ -0,0 +1,585 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Shared design system for all Omni web surfaces.
+--
+-- Modern terminal aesthetic: dark-first, monospace-forward, mobile-optimized.
+-- Designed for iOS webapp usage with focus on reading long content.
+--
+-- Uses Clay for type-checked CSS generation.  Import 'css' for the full
+-- stylesheet text, or use individual token values for composition.
+--
+-- : dep clay
+-- : dep lucid
+module Omni.Web.Style
+  ( -- * Full rendered CSS
+    css,
+
+    -- * Design tokens (re-export for other Clay modules)
+    cBg,
+    cBgRaised,
+    cBgHover,
+    cBgActive,
+    cBorder,
+    cBorderSubtle,
+    cFg,
+    cFgMuted,
+    cFgFaint,
+    cAccent,
+    cAccentDim,
+    cLink,
+    cGreen,
+    cYellow,
+    cOrange,
+    cRed,
+    cPurple,
+    cBlue,
+    fontMono,
+    fontSans,
+    fontReading,
+    radiusSm,
+    radiusMd,
+    radiusLg,
+
+    -- * Shared HTML components
+    sharedNav,
+    sharedShell,
+  )
+where
+
+import Alpha hiding (all, filter, head, not, rem, reverse, (**), (|>))
+import Clay
+import qualified Clay.Media as Media
+import qualified Clay.Stylesheet as Stylesheet
+import qualified Data.Text.Lazy as LazyText
+import qualified Lucid
+
+-- ============================================================================
+-- Design Tokens
+-- ============================================================================
+
+-- Dark-first palette.  Light mode overrides via @media query below.
+
+cBg, cBgRaised, cBgHover, cBgActive :: Color
+cBg = "#0a0a0a"
+cBgRaised = "#141414"
+cBgHover = "#1a1a1a"
+cBgActive = "#222222"
+
+cBorder, cBorderSubtle :: Color
+cBorder = "#262626"
+cBorderSubtle = "#1c1c1c"
+
+cFg, cFgMuted, cFgFaint :: Color
+cFg = "#e0e0e0"
+cFgMuted = "#737373"
+cFgFaint = "#525252"
+
+cAccent, cAccentDim, cLink :: Color
+cAccent = "#22d3ee"
+cAccentDim = "#0e7490"
+cLink = "#22d3ee"
+
+cGreen, cYellow, cOrange, cRed, cPurple, cBlue :: Color
+cGreen = "#4ade80"
+cYellow = "#facc15"
+cOrange = "#fb923c"
+cRed = "#f87171"
+cPurple = "#a78bfa"
+cBlue = "#60a5fa"
+
+fontMono :: [Text]
+fontMono =
+  [ "SF Mono",
+    "Cascadia Code",
+    "JetBrains Mono",
+    "Fira Code",
+    "Menlo",
+    "Monaco",
+    "Courier New",
+    "monospace"
+  ]
+
+fontSans :: [Text]
+fontSans =
+  [ "-apple-system",
+    "BlinkMacSystemFont",
+    "SF Pro Text",
+    "Inter",
+    "system-ui",
+    "sans-serif"
+  ]
+
+fontReading :: [Text]
+fontReading =
+  [ "SF Pro Text",
+    "Inter",
+    "-apple-system",
+    "BlinkMacSystemFont",
+    "system-ui",
+    "sans-serif"
+  ]
+
+radiusSm, radiusMd, radiusLg :: Size LengthUnit
+radiusSm = px 4
+radiusMd = px 6
+radiusLg = px 10
+
+-- ============================================================================
+-- CSS Generation
+-- ============================================================================
+
+css :: LazyText.Text
+css = render stylesheet
+
+stylesheet :: Css
+stylesheet = do
+  resetStyles
+  baseStyles
+  typographyStyles
+  readingStyles
+  layoutStyles
+  navStyles
+  cardStyles
+  badgeStyles
+  buttonStyles
+  formStyles
+  tableStyles
+  utilityStyles
+  mobileStyles
+  iosStyles
+  scrollbarStyles
+  selectionStyles
+
+-- ── Reset ──────────────────────────────────────────────────────────
+
+resetStyles :: Css
+resetStyles = do
+  star <> star # before <> star # after ? do
+    boxSizing borderBox
+    margin nil nil nil nil
+    padding nil nil nil nil
+  html ? do
+    Stylesheet.key "-webkit-text-size-adjust" ("100%" :: Text)
+    Stylesheet.key "-webkit-font-smoothing" ("antialiased" :: Text)
+    Stylesheet.key "-moz-osx-font-smoothing" ("grayscale" :: Text)
+    Stylesheet.key "text-rendering" ("optimizeLegibility" :: Text)
+
+-- ── Base ───────────────────────────────────────────────────────────
+
+baseStyles :: Css
+baseStyles =
+  body ? do
+    fontFamily fontMono [monospace]
+    fontSize (px 13)
+    lineHeight (unitless 1.6)
+    color cFg
+    backgroundColor cBg
+    minHeight (vh 100)
+    Stylesheet.key "min-height" ("100dvh" :: Text)
+    Stylesheet.key "padding-bottom" ("env(safe-area-inset-bottom, 0)" :: Text)
+
+-- ── Typography ─────────────────────────────────────────────────────
+
+typographyStyles :: Css
+typographyStyles = do
+  a ? do
+    color cLink
+    textDecoration none
+  a # hover ? do
+    textDecoration underline
+    Stylesheet.key "text-underline-offset" ("2px" :: Text)
+
+  (h1 <> h2 <> h3 <> h4) ? do
+    fontFamily fontMono [monospace]
+    fontWeight (weight 600)
+    Stylesheet.key "letter-spacing" ("-0.02em" :: Text)
+    lineHeight (unitless 1.3)
+  h1 ? do
+    fontSize (rem 1.25)
+    marginBottom (px 8)
+  h2 ? do
+    fontSize (rem 1.1)
+    marginBottom (px 8)
+    color cFg
+  h3 ? do
+    fontSize (rem 0.95)
+    marginBottom (px 4)
+
+  code ? do
+    fontSize (em 0.9)
+    backgroundColor cBgHover
+    padding (px 1) (px 5) (px 1) (px 5)
+    borderRadius radiusSm radiusSm radiusSm radiusSm
+    border (px 1) solid cBorder
+  pre ? do
+    fontSize (px 12)
+    backgroundColor cBg
+    border (px 1) solid cBorder
+    padding (px 8) (px 16) (px 8) (px 16)
+    borderRadius radiusMd radiusMd radiusMd radiusMd
+    overflow auto
+    whiteSpace preWrap
+    Stylesheet.key "word-break" ("break-all" :: Text)
+  (pre |> code) ? do
+    backgroundColor transparent
+    border nil none transparent
+    padding nil nil nil nil
+    fontSize inherit
+
+-- ── Reading typography (articles, descriptions) ────────────────────
+
+readingStyles :: Css
+readingStyles = do
+  ".reading" ? do
+    fontFamily fontReading [sansSerif]
+    fontSize (px 16)
+    lineHeight (unitless 1.7)
+    Stylesheet.key "letter-spacing" ("-0.011em" :: Text)
+    maxWidth (px 680)
+  ".reading" |> p ? marginBottom (em 1)
+  ".reading" |> h1 ? do
+    fontSize (rem 1.5)
+    margin (em 1.5) nil (em 0.5) nil
+    fontFamily fontReading [sansSerif]
+    fontWeight bold
+  ".reading" |> h2 ? do
+    fontSize (rem 1.25)
+    margin (em 1.25) nil (em 0.4) nil
+    fontFamily fontReading [sansSerif]
+    fontWeight (weight 600)
+  ".reading" |> h3 ? do
+    fontSize (rem 1.1)
+    margin (em 1) nil (em 0.3) nil
+    fontFamily fontReading [sansSerif]
+    fontWeight (weight 600)
+  (".reading" |> ul) <> (".reading" |> ol) ? do
+    margin (em 0.5) nil (em 1) nil
+    paddingLeft (em 1.5)
+  ".reading" ** li ? marginBottom (em 0.3)
+  ".reading" |> "blockquote" ? do
+    borderLeft (px 3) solid cAccentDim
+    paddingLeft (px 16)
+    color cFgMuted
+    margin (em 1) nil (em 1) nil
+  ".reading" ** img ? do
+    maxWidth (pct 100)
+    height auto
+    borderRadius radiusMd radiusMd radiusMd radiusMd
+  ".reading" ** a ? do
+    Stylesheet.key "text-underline-offset" ("3px" :: Text)
+    Stylesheet.key "text-decoration-thickness" ("1px" :: Text)
+  (".reading" ** a)
+    # hover
+    ? Stylesheet.key "text-decoration-thickness" ("2px" :: Text)
+
+-- ── Layout ─────────────────────────────────────────────────────────
+
+layoutStyles :: Css
+layoutStyles = do
+  ".container" ? do
+    width (pct 100)
+    maxWidth (px 960)
+    margin nil auto nil auto
+    padding (px 8) (px 16) (px 8) (px 16)
+  ".container-narrow" ? do
+    width (pct 100)
+    maxWidth (px 680)
+    margin nil auto nil auto
+    padding (px 8) (px 16) (px 8) (px 16)
+
+-- ── Navigation ─────────────────────────────────────────────────────
+
+navStyles :: Css
+navStyles = do
+  ".on-nav" ? do
+    display flex
+    alignItems center
+    Stylesheet.key "gap" ("4px" :: Text)
+    padding (px 8) (px 16) (px 8) (px 16)
+    borderBottom (px 1) solid cBorder
+    backgroundColor cBg
+    position sticky
+    top nil
+    zIndex 100
+    Stylesheet.key "backdrop-filter" ("blur(12px)" :: Text)
+    Stylesheet.key "-webkit-backdrop-filter" ("blur(12px)" :: Text)
+  ".on-nav-brand" ? do
+    fontWeight bold
+    fontSize (px 14)
+    color cAccent
+    textDecoration none
+    Stylesheet.key "letter-spacing" ("0.05em" :: Text)
+    marginRight (px 8)
+  ".on-nav-brand" # hover ? do
+    textDecoration none
+    opacity 0.8
+  ".on-nav-links" ? do
+    display flex
+    Stylesheet.key "gap" ("2px" :: Text)
+  ".on-nav-link" ? do
+    fontSize (px 12)
+    color cFgMuted
+    padding (px 4) (px 10) (px 4) (px 10)
+    borderRadius radiusSm radiusSm radiusSm radiusSm
+    transition "all" (ms 150) ease (sec 0)
+  ".on-nav-link" # hover ? do
+    backgroundColor cBgHover
+    color cFg
+    textDecoration none
+  ".on-active" ? do
+    color cAccent
+    backgroundColor cBgHover
+
+-- ── Cards ──────────────────────────────────────────────────────────
+
+cardStyles :: Css
+cardStyles = do
+  ".card" ? do
+    backgroundColor cBgRaised
+    border (px 1) solid cBorder
+    borderRadius radiusMd radiusMd radiusMd radiusMd
+    padding (px 8) (px 16) (px 8) (px 16)
+  ".card-link" ? do
+    display block
+    textDecoration none
+    color inherit
+  ".card-link" # hover ? textDecoration none
+  (".card-link" # hover |> ".card") ? borderColor cAccentDim
+
+-- ── Badges ─────────────────────────────────────────────────────────
+
+badgeStyles :: Css
+badgeStyles = do
+  ".badge" ? do
+    display inlineBlock
+    fontSize (px 11)
+    fontFamily fontMono [monospace]
+    fontWeight (weight 500)
+    padding (px 2) (px 8) (px 2) (px 8)
+    borderRadius radiusSm radiusSm radiusSm radiusSm
+    Stylesheet.key "letter-spacing" ("0.03em" :: Text)
+    whiteSpace nowrap
+  ".badge-open" ? do
+    Stylesheet.key "background" ("rgba(250,204,21,0.15)" :: Text)
+    color cYellow
+  ".badge-inprogress" ? do
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+    color cBlue
+  ".badge-review" ? do
+    Stylesheet.key "background" ("rgba(167,139,250,0.15)" :: Text)
+    color cPurple
+  ".badge-approved" ? do
+    Stylesheet.key "background" ("rgba(34,211,238,0.15)" :: Text)
+    color cAccent
+  ".badge-done" ? do
+    Stylesheet.key "background" ("rgba(74,222,128,0.15)" :: Text)
+    color cGreen
+  ".badge-needshelp" ? do
+    Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+    color cOrange
+  ".badge-p0" ? do
+    Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+    color cRed
+  ".badge-p1" ? do
+    Stylesheet.key "background" ("rgba(251,146,60,0.15)" :: Text)
+    color cOrange
+  ".badge-p2" ? do
+    Stylesheet.key "background" ("rgba(96,165,250,0.15)" :: Text)
+    color cBlue
+  ".badge-p3" ? do
+    backgroundColor cBgHover
+    color cFgMuted
+  ".badge-p4" ? do
+    backgroundColor cBgHover
+    color cFgFaint
+
+-- ── Buttons ────────────────────────────────────────────────────────
+
+buttonStyles :: Css
+buttonStyles = do
+  ".btn" ? do
+    display inlineFlex
+    alignItems center
+    justifyContent center
+    Stylesheet.key "gap" ("4px" :: Text)
+    minHeight (px 36)
+    padding (px 6) (px 14) (px 6) (px 14)
+    fontFamily fontMono [monospace]
+    fontSize (px 12)
+    fontWeight (weight 500)
+    border (px 1) solid cBorder
+    borderRadius radiusMd radiusMd radiusMd radiusMd
+    backgroundColor cBgRaised
+    color cFg
+    cursor pointer
+    transition "all" (ms 150) ease (sec 0)
+    textDecoration none
+    Stylesheet.key "-webkit-tap-highlight-color" ("transparent" :: Text)
+    Stylesheet.key "touch-action" ("manipulation" :: Text)
+  ".btn" # hover ? do
+    backgroundColor cBgHover
+    borderColor cFgFaint
+    textDecoration none
+  ".btn" # active ? backgroundColor cBgActive
+  ".btn-primary" ? do
+    backgroundColor cAccentDim
+    color white
+    borderColor cAccentDim
+  ".btn-primary" # hover ? do
+    backgroundColor cAccent
+    borderColor cAccent
+    color black
+  ".btn-danger" ? do
+    Stylesheet.key "background" ("rgba(248,113,113,0.15)" :: Text)
+    color cRed
+    Stylesheet.key "border-color" ("rgba(248,113,113,0.3)" :: Text)
+  ".btn-danger"
+    # hover
+    ? Stylesheet.key "background" ("rgba(248,113,113,0.25)" :: Text)
+  ".btn-ghost" ? do
+    backgroundColor transparent
+    borderColor transparent
+  ".btn-ghost" # hover ? backgroundColor cBgHover
+
+-- ── Forms ──────────────────────────────────────────────────────────
+
+formStyles :: Css
+formStyles = do
+  (input <> select <> textarea) ? do
+    fontFamily fontMono [monospace]
+    fontSize (px 13)
+    color cFg
+    backgroundColor cBg
+    border (px 1) solid cBorder
+    borderRadius radiusMd radiusMd radiusMd radiusMd
+    padding (px 8) (px 12) (px 8) (px 12)
+    Stylesheet.key "outline" ("none" :: Text)
+    transition "border-color" (ms 150) ease (sec 0)
+  (input # focus <> select # focus <> textarea # focus) ? do
+    borderColor cAccentDim
+    Stylesheet.key "box-shadow" ("0 0 0 2px rgba(34,211,238,0.1)" :: Text)
+  textarea ? do
+    Stylesheet.key "resize" ("vertical" :: Text)
+    minHeight (px 80)
+
+-- ── Tables ─────────────────────────────────────────────────────────
+
+tableStyles :: Css
+tableStyles = do
+  table ? do
+    width (pct 100)
+    Stylesheet.key "border-collapse" ("collapse" :: Text)
+  (th <> td) ? do
+    textAlign (alignSide sideLeft)
+    padding (px 8) (px 12) (px 8) (px 12)
+    borderBottom (px 1) solid cBorder
+  th ? do
+    color cFgMuted
+    fontSize (px 11)
+    textTransform uppercase
+    Stylesheet.key "letter-spacing" ("0.06em" :: Text)
+    fontWeight (weight 500)
+  tr # hover ? backgroundColor cBgHover
+  (tr # lastChild |> td) ? borderBottom nil none transparent
+
+-- ── Utility classes ────────────────────────────────────────────────
+
+utilityStyles :: Css
+utilityStyles = do
+  ".muted" ? color cFgMuted
+  ".faint" ? color cFgFaint
+  ".accent" ? color cAccent
+  ".text-sm" ? fontSize (px 12)
+  ".text-xs" ? fontSize (px 11)
+  ".text-sans" ? fontFamily fontSans [sansSerif]
+  ".empty-state" ? do
+    color cFgFaint
+    fontStyle italic
+    padding (px 16) nil (px 16) nil
+  ".divider" ? do
+    border nil none transparent
+    borderTop (px 1) solid cBorder
+    margin (px 16) nil (px 16) nil
+
+-- ── Mobile responsive ──────────────────────────────────────────────
+
+mobileStyles :: Css
+mobileStyles =
+  query Media.screen [Media.maxWidth (px 640)] <| do
+    body ? fontSize (px 14)
+    ".container" <> ".container-narrow" ? padding (px 8) (px 8) (px 8) (px 8)
+    ".on-nav" ? padding (px 8) (px 8) (px 8) (px 8)
+    ".on-nav-link" ? do
+      padding (px 6) (px 8) (px 6) (px 8)
+      fontSize (px 13)
+    ".on-nav-brand" ? fontSize (px 15)
+    ".card" ? padding (px 8) (px 8) (px 8) (px 8)
+    h1 ? fontSize (rem 1.15)
+    ".reading" ? do
+      fontSize (px 15)
+      lineHeight (unitless 1.65)
+    ".hide-mobile" ? display none
+
+-- ── iOS safe areas ─────────────────────────────────────────────────
+
+iosStyles :: Css
+iosStyles = do
+  Stylesheet.key
+    "@supports (padding: env(safe-area-inset-top))"
+    ( "{ .on-nav { padding-top: max(8px, env(safe-area-inset-top)); }"
+        <> " .container, .container-narrow { padding-left: max(16px, env(safe-area-inset-left));"
+        <> " padding-right: max(16px, env(safe-area-inset-right)); } }" ::
+        Text
+    )
+
+-- ── Scrollbar ──────────────────────────────────────────────────────
+
+scrollbarStyles :: Css
+scrollbarStyles = do
+  Stylesheet.key "::-webkit-scrollbar" ("{ width: 6px; height: 6px; }" :: Text)
+  Stylesheet.key "::-webkit-scrollbar-track" ("{ background: transparent; }" :: Text)
+  Stylesheet.key "::-webkit-scrollbar-thumb" ("{ background: #262626; border-radius: 3px; }" :: Text)
+
+-- ── Selection ──────────────────────────────────────────────────────
+
+selectionStyles :: Css
+selectionStyles =
+  Stylesheet.key "::selection" ("{ background: rgba(34,211,238,0.3); }" :: Text)
+
+-- ============================================================================
+-- Shared HTML Components
+-- ============================================================================
+
+-- | Navigation bar shared across all surfaces.
+sharedNav :: Text -> Lucid.Html ()
+sharedNav activePage =
+  Lucid.nav_ [Lucid.class_ "on-nav"] <| do
+    Lucid.a_ [Lucid.href_ "/tasks", Lucid.class_ "on-nav-brand"] "omni"
+    Lucid.div_ [Lucid.class_ "on-nav-links"] <| do
+      navLink "/tasks" "tasks" (activePage == "tasks")
+      navLink "/news/" "news" (activePage == "news")
+      navLink "/files/" "files" (activePage == "files")
+  where
+    navLink href' label' isActive' =
+      Lucid.a_
+        [ Lucid.href_ href',
+          Lucid.class_ ("on-nav-link" <> if isActive' then " on-active" else "")
+        ]
+        label'
+
+-- | Shared page shell with design-system CSS and iOS webapp meta tags.
+sharedShell :: Text -> Text -> Lucid.Html () -> Lucid.Html ()
+sharedShell title' activePage' content' =
+  Lucid.doctypehtml_ <| do
+    Lucid.head_ <| do
+      Lucid.meta_ [Lucid.charset_ "utf-8"]
+      Lucid.meta_ [Lucid.name_ "viewport", Lucid.content_ "width=device-width, initial-scale=1, viewport-fit=cover"]
+      Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-capable", Lucid.content_ "yes"]
+      Lucid.meta_ [Lucid.name_ "apple-mobile-web-app-status-bar-style", Lucid.content_ "black-translucent"]
+      Lucid.meta_ [Lucid.name_ "theme-color", Lucid.content_ "#0a0a0a"]
+      Lucid.title_ (Lucid.toHtml title')
+      Lucid.style_ (LazyText.toStrict css)
+    Lucid.body_ <| do
+      sharedNav activePage'
+      content'