diff --git a/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md b/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md
new file mode 100644
index 0000000..99885eb
--- /dev/null
+++ b/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md
@@ -0,0 +1,1670 @@
+# UX/UI Overhaul Implementation Plan
+
+> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Restructure navigation (17→9 items), unify content pipeline, redesign dashboard, standardize UX patterns, and add premium polish across the marketing app.
+
+**Architecture:** 6 independent phases, each deployable on its own. Phase 1 restructures nav + consistency. Phase 2-3 build the Content pipeline. Phase 4 enhances campaigns. Phase 5 redesigns the dashboard. Phase 6 adds premium animations and effects.
+
+**Tech Stack:** React (Vite), Tailwind CSS, Express.js, NocoDB REST API, Lucide icons, CSS animations (no motion libraries).
+
+**Spec:** `docs/superpowers/specs/2026-03-11-ux-ui-overhaul-design.md`
+
+---
+
+## Chunk 1: Phase 1 — Navigation Reorganization
+
+### Task 1.1: Rewrite Sidebar to Flat Nav
+
+**Files:**
+- Modify: `client/src/components/Sidebar.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Replace nav data structure**
+
+Replace the `standaloneTop`, `moduleGroups`, `standaloneBottom` arrays (lines 11-63) with a flat nav config:
+
+```jsx
+const navItems = [
+ { to: '/', icon: LayoutDashboard, labelKey: 'nav.dashboard', end: true },
+ 'divider',
+ { to: '/campaigns', icon: Calendar, labelKey: 'nav.campaigns', module: 'marketing' },
+ { to: '/content', icon: FileEdit, labelKey: 'nav.content', module: 'marketing' },
+ 'divider',
+ { to: '/projects', icon: FolderKanban, labelKey: 'nav.projects', module: 'projects', hint: 'nav.projectsHint' },
+ { to: '/issues', icon: AlertCircle, labelKey: 'nav.issues', module: 'issues' },
+ 'divider',
+ { to: '/finance', icon: BarChart3, labelKey: 'nav.finance', module: 'finance', minRole: 'manager', hint: 'nav.financeHint' },
+ 'divider',
+ { to: '/team', icon: Users, labelKey: 'nav.team' },
+ { to: '/settings', icon: Settings, labelKey: 'nav.settings' },
+]
+```
+
+- [ ] **Step 2: Rewrite nav rendering**
+
+Remove the module group expand/collapse logic (`expandedGroups`, `toggleGroup`, the `visibleGroups` map). Replace with a flat list renderer:
+
+```jsx
+const filteredItems = navItems.filter(item => {
+ if (item === 'divider') return true
+ if (item.module && !hasModule(item.module)) return false
+ if (item.minRole && userLevel < (ROLE_LEVEL[item.minRole] ?? 0)) return false
+ return true
+})
+
+// In JSX, replace the nav section:
+{filteredItems.map((item, i) =>
+ item === 'divider'
+ ?
+ : navLink(item)
+)}
+```
+
+- [ ] **Step 3: Remove sub-item indentation from navLink**
+
+Update the `navLink` function (line 83) — remove the `sub` parameter and sub-item styles. All items are top-level now:
+
+```jsx
+const navLink = ({ to, icon: Icon, labelKey, end, hint }) => (
+
+ `flex items-center gap-3 rounded-lg font-medium transition-all duration-200 group px-3 py-2 text-sm ${
+ isActive
+ ? 'bg-white/15 text-white shadow-sm sidebar-active-glow'
+ : 'text-text-on-dark-muted hover:bg-white/8 hover:text-white'
+ }`
+ }
+ >
+
+ {!collapsed && (
+ {t(labelKey)}
+ )}
+ {!collapsed && hint && (
+ {t(hint)}
+ )}
+
+)
+```
+
+- [ ] **Step 4: Remove unused imports and state**
+
+Remove: `ChevronDown` from lucide imports, `useState` for `expandedGroups`, the `toggleGroup` function, the `visibleGroups` computation.
+
+Add to imports if missing: `Settings` from lucide-react.
+
+- [ ] **Step 5: Add i18n keys**
+
+In `en.json`, add:
+```json
+"nav.content": "Content",
+"nav.projectsHint": "+ tasks",
+"nav.financeHint": "+ budgets"
+```
+
+In `ar.json`, add:
+```json
+"nav.content": "المحتوى",
+"nav.projectsHint": "+ المهام",
+"nav.financeHint": "+ الميزانيات"
+```
+
+- [ ] **Step 6: Test manually — verify sidebar renders**
+
+Run: `cd /home/fahed/clawd/projects/marketing-app && npm run dev`
+
+Check:
+- Dashboard, Campaigns, Content, Projects, Issues, Finance, Team, Settings all visible
+- Dividers appear between groups
+- No collapsible module headers
+- Collapsed sidebar shows icons correctly
+- Active glow works on each item
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add client/src/components/Sidebar.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "refactor: flatten sidebar nav — 17 items to 9, remove collapsible groups"
+```
+
+### Task 1.2: Add Route Redirects for Old URLs
+
+**Files:**
+- Modify: `client/src/App.jsx`
+
+- [ ] **Step 1: Add Navigate import and redirect routes**
+
+At the top of App.jsx, ensure `Navigate` is imported from react-router-dom.
+
+Inside the protected ` }>` block, add redirects for removed routes:
+
+```jsx
+{/* Redirects for old routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+```
+
+- [ ] **Step 2: Add Content route placeholder**
+
+Add a route for the Content page (will be a placeholder until Phase 2):
+
+```jsx
+const Content = lazy(() => import('./pages/Content'))
+
+// Inside protected routes — add BEFORE the redirect routes:
+ }> } />
+ }> } />
+```
+
+**Also remove the old standalone route definitions** for `/posts`, `/calendar`, `/artefacts`, `/translations`, `/assets`, `/brands`, `/tasks`, `/budgets` — the redirects replace them.
+
+- [ ] **Step 3: Create placeholder Content page**
+
+Create `client/src/pages/Content.jsx`:
+
+```jsx
+import { useParams, useSearchParams } from 'react-router-dom'
+import { useLanguage } from '../i18n/LanguageContext'
+
+const TABS = ['pipeline', 'copy', 'translations', 'design', 'posts']
+
+export default function Content() {
+ const { tab = 'pipeline' } = useParams()
+ const [searchParams] = useSearchParams()
+ const { t } = useLanguage()
+
+ return (
+
+
{t('nav.content')}
+
+
Content pipeline — coming soon.
+
+ )
+}
+```
+
+- [ ] **Step 4: Update Header page title map**
+
+In `client/src/components/Header.jsx`, find the `PAGE_TITLE_KEYS` map and add:
+
+```js
+'/content': 'header.content',
+```
+
+Add to both i18n files: `"header.content": "Content"` (en), `"header.content": "المحتوى"` (ar).
+
+Remove entries for `/posts`, `/calendar`, `/artefacts`, `/translations`, `/assets`, `/brands`, `/tasks`, `/budgets` (these now redirect).
+
+- [ ] **Step 5: Test redirects**
+
+Navigate to `/posts` → should redirect to `/content/posts`
+Navigate to `/artefacts` → should redirect to `/content/design`
+Navigate to `/content` → should show placeholder with tabs
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add client/src/App.jsx client/src/pages/Content.jsx client/src/components/Header.jsx
+git commit -m "feat: add route redirects for nav reorg, placeholder Content page"
+```
+
+### Task 1.3: Absorb Budgets into Finance Page
+
+**Files:**
+- Modify: `client/src/pages/Finance.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Read Finance.jsx and Budgets.jsx**
+
+Read both files to understand current structure and plan the merge.
+
+- [ ] **Step 2: Add tab toggle to Finance page**
+
+At the top of the Finance page, add a tab bar switching between "Dashboard" and "Budgets":
+
+```jsx
+const [searchParams, setSearchParams] = useSearchParams()
+const activeTab = searchParams.get('tab') || 'dashboard'
+```
+
+Render tabs:
+```jsx
+
+ setSearchParams({})}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ activeTab === 'dashboard'
+ ? 'border-brand-primary text-brand-primary'
+ : 'border-transparent text-text-secondary hover:text-text-primary'
+ }`}
+ >
+ {t('nav.financeDashboard')}
+
+ setSearchParams({ tab: 'budgets' })}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ activeTab === 'budgets'
+ ? 'border-brand-primary text-brand-primary'
+ : 'border-transparent text-text-secondary hover:text-text-primary'
+ }`}
+ >
+ {t('nav.budgets')}
+
+
+```
+
+- [ ] **Step 3: Conditionally render content**
+
+Wrap the existing Finance dashboard content in `{activeTab === 'dashboard' && (...)}`.
+
+Import the Budgets page component and render: `{activeTab === 'budgets' && }`.
+
+Alternatively, if Budgets is too large to import directly, lazy-load it.
+
+- [ ] **Step 4: Test both tabs work**
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/pages/Finance.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: absorb Budgets as tab inside Finance page"
+```
+
+### Task 1.4: Absorb Brands & Assets into Settings Page
+
+**Files:**
+- Modify: `client/src/pages/Settings.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Read Settings.jsx, Brands.jsx, Assets.jsx**
+
+Understand current Settings structure and what needs to be added.
+
+- [ ] **Step 2: Add tab navigation to Settings**
+
+Same pattern as Finance: tabs for "General", "Brands", "Assets" using `useSearchParams`.
+
+```jsx
+const tabs = [
+ { key: 'general', labelKey: 'settings.general' },
+ { key: 'brands', labelKey: 'nav.brands' },
+ { key: 'assets', labelKey: 'nav.assets' },
+]
+```
+
+- [ ] **Step 3: Render tab content**
+
+- General tab: existing Settings content
+- Brands tab: import and render Brands component
+- Assets tab: import and render Assets component
+
+- [ ] **Step 4: Add i18n key for "General"**
+
+```json
+"settings.general": "General" // en
+"settings.general": "عام" // ar
+```
+
+- [ ] **Step 5: Test all three tabs**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add client/src/pages/Settings.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: absorb Brands & Assets as tabs inside Settings page"
+```
+
+### Task 1.5: Absorb Tasks into Projects Page
+
+**Files:**
+- Modify: `client/src/pages/Projects.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Read Projects.jsx and Tasks.jsx**
+
+- [ ] **Step 2: Add "Projects" / "All Tasks" tab toggle to Projects page**
+
+Same `useSearchParams` pattern. Default tab is "projects".
+
+- [ ] **Step 3: Render Tasks component in "All Tasks" tab**
+
+Import Tasks and render when `tab === 'tasks'`.
+
+- [ ] **Step 4: Add i18n keys**
+
+```json
+"projects.allTasks": "All Tasks" // en
+"projects.allTasks": "جميع المهام" // ar
+```
+
+- [ ] **Step 5: Test both tabs**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add client/src/pages/Projects.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: absorb Tasks as tab inside Projects page"
+```
+
+### Task 1.6: Standardize Page Headers
+
+**Files:**
+- Modify: All page files (PostProduction/Content, Issues, Projects, Finance, Campaigns, Team, Settings, Dashboard)
+- Create: `client/src/components/PageHeader.jsx`
+
+- [ ] **Step 1: Create shared PageHeader component**
+
+```jsx
+import { Search, X } from 'lucide-react'
+import { useState } from 'react'
+import { useLanguage } from '../i18n/LanguageContext'
+
+export default function PageHeader({
+ title,
+ searchTerm,
+ onSearchChange,
+ filters, // ReactNode — inline filter dropdowns
+ viewToggle, // ReactNode — view mode buttons
+ actions, // ReactNode — create button, bulk actions
+}) {
+ const [searchOpen, setSearchOpen] = useState(!!searchTerm)
+ const { t } = useLanguage()
+
+ return (
+
+
{title}
+
+
+ {onSearchChange && (
+ searchOpen ? (
+
+
+ onSearchChange(e.target.value)}
+ placeholder={t('common.search')}
+ className="pl-9 pr-8 py-1.5 text-sm border border-border rounded-lg bg-surface-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 w-48"
+ autoFocus
+ />
+ { onSearchChange(''); setSearchOpen(false) }}
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-primary"
+ >
+
+
+
+ ) : (
+
setSearchOpen(true)}
+ className="p-2 text-text-secondary hover:text-text-primary hover:bg-surface-secondary rounded-lg transition-colors"
+ >
+
+
+ )
+ )}
+
+ {filters}
+ {viewToggle}
+ {actions}
+
+ )
+}
+```
+
+- [ ] **Step 2: Adopt PageHeader in Issues.jsx as first migration**
+
+Replace the existing header JSX in Issues.jsx with ``, passing the existing search, filters, view toggle, and create button as props.
+
+- [ ] **Step 3: Adopt PageHeader in remaining pages**
+
+Apply the same pattern to: PostProduction (now Content/Posts tab), Projects, Campaigns, Finance, Team.
+
+- [ ] **Step 4: Standardize filter visibility**
+
+Ensure all pages show filters inline (always visible). Remove any toggle buttons that show/hide the filter bar.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/components/PageHeader.jsx client/src/pages/*.jsx
+git commit -m "feat: standardize page headers with shared PageHeader component"
+```
+
+### Task 1.7: Standardize Detail Panel Layout
+
+**Files:**
+- Modify: `client/src/components/SlidePanel.jsx`
+- Modify: All detail panel components
+
+- [ ] **Step 1: Update SlidePanel default maxWidth**
+
+In `SlidePanel.jsx`, change default `maxWidth` from `'420px'` to `'480px'`.
+
+- [ ] **Step 2: Standardize header pattern across panels**
+
+Each detail panel should use this header pattern:
+```jsx
+
+
+
+
+
{title}
+
+ {t('common.save')}
+
+
+
+```
+
+- [ ] **Step 3: Move delete buttons into overflow menus**
+
+In each detail panel (PostDetailPanel, ArtefactDetailPanel, IssueDetailPanel, TaskDetailPanel, TranslationDetailPanel, CampaignDetailPanel), move the delete button from standalone position into an overflow `⋯` menu.
+
+Create a small `OverflowMenu` component if one doesn't exist:
+
+```jsx
+// client/src/components/OverflowMenu.jsx
+import { useState, useRef, useEffect } from 'react'
+import { MoreVertical } from 'lucide-react'
+
+export default function OverflowMenu({ items }) {
+ const [open, setOpen] = useState(false)
+ const ref = useRef()
+
+ useEffect(() => {
+ const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
+ document.addEventListener('mousedown', handler)
+ return () => document.removeEventListener('mousedown', handler)
+ }, [])
+
+ return (
+
+
setOpen(!open)}
+ className="p-1.5 hover:bg-surface-secondary rounded-lg transition-colors"
+ >
+
+
+ {open && (
+
+ {items.map((item, i) => (
+ { item.onClick(); setOpen(false) }}
+ className={`w-full text-left px-3 py-2 text-sm hover:bg-surface-secondary transition-colors ${
+ item.danger ? 'text-red-600 hover:bg-red-50' : 'text-text-primary'
+ }`}
+ >
+ {item.label}
+
+ ))}
+
+ )}
+
+ )
+}
+```
+
+- [ ] **Step 4: Standardize tab order in detail panels**
+
+Ensure every detail panel with tabs uses: Details → Activity → Approval (in that order). Rename any "Discussion" or "Comments" tabs to "Activity".
+
+- [ ] **Step 5: Test each detail panel opens and functions correctly**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add client/src/components/SlidePanel.jsx client/src/components/OverflowMenu.jsx client/src/components/*Panel*.jsx client/src/components/*DetailPanel*.jsx
+git commit -m "feat: standardize detail panels — 480px width, overflow menu, consistent tabs"
+```
+
+---
+
+## Chunk 2: Phase 2 — Content Page with Tabs
+
+### Task 2.1: Build Content Page Shell with Tab Routing
+
+**Files:**
+- Modify: `client/src/pages/Content.jsx` (replace placeholder)
+- Modify: `client/src/App.jsx`
+
+- [ ] **Step 1: Update Content.jsx with full tab system**
+
+```jsx
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
+import { useLanguage } from '../i18n/LanguageContext'
+import { Layers, PenLine, Languages, Palette, Send } from 'lucide-react'
+
+const TABS = [
+ { key: 'pipeline', labelKey: 'content.pipeline', icon: Layers },
+ { key: 'copy', labelKey: 'content.copy', icon: PenLine },
+ { key: 'translations', labelKey: 'content.translations', icon: Languages },
+ { key: 'design', labelKey: 'content.design', icon: Palette },
+ { key: 'posts', labelKey: 'content.posts', icon: Send },
+]
+
+export default function Content() {
+ const { tab = 'pipeline' } = useParams()
+ const navigate = useNavigate()
+ const [searchParams] = useSearchParams()
+ const { t } = useLanguage()
+
+ return (
+
+ {/* Tab bar */}
+
+ {TABS.map(({ key, labelKey, icon: Icon }) => (
+ navigate(`/content/${key}`)}
+ className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
+ tab === key
+ ? 'border-brand-primary text-brand-primary'
+ : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border'
+ }`}
+ >
+
+ {t(labelKey)}
+
+ ))}
+
+
+ {/* Tab content */}
+ {tab === 'pipeline' &&
}
+ {tab === 'copy' &&
}
+ {tab === 'translations' &&
}
+ {tab === 'design' &&
}
+ {tab === 'posts' &&
}
+
+ )
+}
+
+// Placeholder sub-components — replaced in subsequent tasks
+function PipelineTab() {
+ return Pipeline view — coming in Phase 3.
+}
+
+function CopyTab() {
+ return Copy tab — loading existing translations (originals).
+}
+
+function TranslationsTab() {
+ return Translations tab — loading.
+}
+
+function DesignTab() {
+ return Design tab — loading artefacts.
+}
+
+function PostsTab() {
+ return Posts tab — loading.
+}
+```
+
+- [ ] **Step 2: Add i18n keys**
+
+In `en.json`:
+```json
+"content.pipeline": "Pipeline",
+"content.copy": "Copy",
+"content.translations": "Translations",
+"content.design": "Design",
+"content.posts": "Posts"
+```
+
+In `ar.json`:
+```json
+"content.pipeline": "خط الإنتاج",
+"content.copy": "النسخ",
+"content.translations": "الترجمات",
+"content.design": "التصميم",
+"content.posts": "المنشورات"
+```
+
+- [ ] **Step 3: Test tab navigation**
+
+Navigate to `/content` → Pipeline tab active
+Click each tab → URL updates, correct tab shows
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/pages/Content.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: Content page shell with 5-tab routing"
+```
+
+### Task 2.2: Wire Posts Tab (Migrate PostProduction)
+
+**Files:**
+- Modify: `client/src/pages/Content.jsx`
+- Read: `client/src/pages/PostProduction.jsx`
+
+- [ ] **Step 1: Read PostProduction.jsx fully**
+
+Understand all state, effects, handlers, and JSX.
+
+- [ ] **Step 2: Import and render PostProduction inside PostsTab**
+
+The simplest approach: import the existing page component and render it directly in the Posts tab. This preserves all existing functionality with zero risk:
+
+```jsx
+import PostProduction from './PostProduction'
+
+function PostsTab() {
+ const [searchParams] = useSearchParams()
+ const defaultView = searchParams.get('view')
+ return
+}
+```
+
+If PostProduction renders its own page title/header, either:
+- Add a `embedded` prop to suppress the title when rendered inside Content
+- Or remove the title from PostProduction and let Content handle it
+
+- [ ] **Step 3: Handle Calendar view toggle**
+
+If URL is `/content/posts?view=calendar`, ensure PostProduction starts in calendar view. Pass `defaultView` prop.
+
+- [ ] **Step 4: Test**
+
+- All post functionality works: create, edit, kanban drag, list view, calendar view
+- Detail panel opens correctly
+- Filters work
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/pages/Content.jsx client/src/pages/PostProduction.jsx
+git commit -m "feat: wire Posts tab in Content page — migrates PostProduction"
+```
+
+### Task 2.3: Wire Translations & Copy Tabs
+
+**Files:**
+- Modify: `client/src/pages/Content.jsx`
+- Modify: `client/src/pages/Translations.jsx`
+
+- [ ] **Step 1: Read Translations.jsx fully**
+
+Understand the data model — particularly how originals vs translations are currently distinguished.
+
+- [ ] **Step 2: Add `is_original` filter prop to Translations component**
+
+Modify Translations.jsx to accept an optional `filterOriginal` prop:
+- `filterOriginal={true}` → show only original copy (Copy tab)
+- `filterOriginal={false}` → show only translations (Translations tab)
+- `filterOriginal={undefined}` → show all (backwards compat)
+
+The filter applies client-side on the loaded data, filtering by `is_original` field. Until the server adds `is_original`, all items show as originals (safe default).
+
+- [ ] **Step 3: Wire into Content.jsx**
+
+```jsx
+import Translations from './Translations'
+
+function CopyTab() {
+ return
+}
+
+function TranslationsTab() {
+ return
+}
+```
+
+- [ ] **Step 4: Test both tabs show data**
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/pages/Content.jsx client/src/pages/Translations.jsx
+git commit -m "feat: wire Copy & Translations tabs in Content page"
+```
+
+### Task 2.4: Wire Design Tab (Migrate Artefacts)
+
+**Files:**
+- Modify: `client/src/pages/Content.jsx`
+- Modify: `client/src/pages/Artefacts.jsx`
+
+- [ ] **Step 1: Import Artefacts into Design tab**
+
+Same approach as Posts — import and render with `embedded` prop:
+
+```jsx
+import Artefacts from './Artefacts'
+
+function DesignTab() {
+ return
+}
+```
+
+- [ ] **Step 2: Add `embedded` prop handling to Artefacts.jsx**
+
+When `embedded={true}`, suppress the page title.
+
+- [ ] **Step 3: Test design tab**
+
+- Grid and list views work
+- Detail panel opens
+- Create artefact works
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/pages/Content.jsx client/src/pages/Artefacts.jsx
+git commit -m "feat: wire Design tab in Content page — migrates Artefacts"
+```
+
+---
+
+## Chunk 3: Phase 3 — Content Item Model + Pipeline
+
+### Task 3.1: Server — ContentItems Table Schema
+
+**Files:**
+- Modify: `server/server.js`
+
+- [ ] **Step 1: Add ContentItems to REQUIRED_TABLES**
+
+Add inside the `REQUIRED_TABLES` object (around line 181-445). The value must be an **array** of column objects (not an object with `table_name`/`columns` keys — that's not the pattern used):
+
+```js
+ContentItems: [
+ { title: 'title', uidt: 'SingleLineText' },
+ { title: 'stage', uidt: 'SingleLineText' }, // copy, translate, design, post, published
+ { title: 'created_by', uidt: 'SingleLineText' },
+],
+```
+
+- [ ] **Step 2: Add FK columns for ContentItems**
+
+In `FK_COLUMNS`, add:
+
+```js
+ContentItems: ['campaign_id', 'brand_id', 'assignee_id'],
+Translations: [...existing, 'content_item_id'],
+Artefacts: [...existing, 'content_item_id'],
+Posts: [...existing, 'content_item_id'],
+```
+
+- [ ] **Step 3: Add is_original to Translations TEXT_COLUMNS**
+
+```js
+Translations: [...existing, { name: 'is_original', uidt: 'Checkbox' }],
+```
+
+- [ ] **Step 4: Test — restart server, verify table + columns created**
+
+Run: `cd /home/fahed/clawd/projects/marketing-app && node server/server.js`
+
+Check logs for:
+- `✓ Created table ContentItems`
+- `✓ Created column content_item_id` on Translations, Artefacts, Posts
+- `✓ Created column is_original` on Translations
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add server/server.js
+git commit -m "feat: add ContentItems table schema + FK columns + is_original flag"
+```
+
+### Task 3.2: Server — ContentItems CRUD Routes
+
+**Files:**
+- Modify: `server/server.js`
+
+- [ ] **Step 1: Add GET /api/content-items**
+
+Follow the existing route pattern (e.g., GET /api/issues). The `nocodb` client uses `nocodb.list('TableName', opts)` — no tableId resolution needed. Server has no `normalize()` — normalization happens client-side in `api.js`.
+
+```js
+app.get('/api/content-items', requireAuth, async (req, res) => {
+ try {
+ const { stage, campaign_id } = req.query
+ const conditions = []
+ if (stage) conditions.push({ field: 'stage', op: 'eq', value: sanitizeWhereValue(stage) })
+ if (campaign_id) conditions.push({ field: 'campaign_id', op: 'eq', value: sanitizeWhereValue(campaign_id) })
+
+ const items = await nocodb.list('ContentItems', {
+ where: conditions,
+ sort: '-CreatedAt',
+ })
+
+ // Enrich with names using batchResolveNames pattern
+ const brandIds = [...new Set(items.filter(i => i.brand_id).map(i => i.brand_id))]
+ const userIds = [...new Set(items.filter(i => i.assignee_id).map(i => i.assignee_id))]
+ const brandMap = await batchResolveNames('Brands', brandIds)
+ const userMap = await batchResolveNames('Users', userIds)
+
+ for (const item of items) {
+ if (item.brand_id) item.brand_name = brandMap[item.brand_id] || ''
+ if (item.assignee_id) item.assignee_name = userMap[item.assignee_id] || ''
+ }
+
+ res.json(items)
+ } catch (err) {
+ console.error('[GET /content-items]', err)
+ res.status(500).json({ error: 'Failed to fetch content items' })
+ }
+})
+```
+
+Note: Check if `batchResolveNames` exists in server.js. If not, use the same enrichment pattern as GET /api/issues (which reads users/brands from cached data).
+
+- [ ] **Step 2: Add POST /api/content-items**
+
+```js
+app.post('/api/content-items', requireAuth, async (req, res) => {
+ try {
+ const created = await nocodb.create('ContentItems', {
+ title: req.body.title,
+ stage: req.body.stage || 'copy',
+ campaign_id: req.body.campaign_id || null,
+ brand_id: req.body.brand_id || null,
+ assignee_id: req.body.assignee_id || null,
+ created_by: String(req.session.userId),
+ })
+ res.status(201).json(created)
+ } catch (err) {
+ console.error('[POST /content-items]', err)
+ res.status(500).json({ error: 'Failed to create content item' })
+ }
+})
+```
+
+- [ ] **Step 3: Add PATCH /api/content-items/:id**
+
+```js
+app.patch('/api/content-items/:id', requireAuth, async (req, res) => {
+ try {
+ const allowed = ['title', 'stage', 'campaign_id', 'brand_id', 'assignee_id']
+ const data = {}
+ for (const key of allowed) {
+ if (req.body[key] !== undefined) data[key] = req.body[key]
+ }
+ await nocodb.update('ContentItems', req.params.id, data)
+ const updated = await nocodb.get('ContentItems', req.params.id)
+ res.json(updated)
+ } catch (err) {
+ console.error('[PATCH /content-items]', err)
+ res.status(500).json({ error: 'Failed to update content item' })
+ }
+})
+```
+
+- [ ] **Step 4: Add DELETE /api/content-items/:id**
+
+```js
+app.delete('/api/content-items/:id', requireAuth, async (req, res) => {
+ try {
+ await nocodb.delete('ContentItems', req.params.id)
+ res.json({ success: true })
+ } catch (err) {
+ console.error('[DELETE /content-items]', err)
+ res.status(500).json({ error: 'Failed to delete content item' })
+ }
+})
+```
+
+- [ ] **Step 5: Test with curl**
+
+```bash
+# Create
+curl -X POST http://localhost:3001/api/content-items -H 'Content-Type: application/json' -H 'Cookie: ...' -d '{"title":"Test Content"}'
+
+# List
+curl http://localhost:3001/api/content-items -H 'Cookie: ...'
+
+# Update
+curl -X PATCH http://localhost:3001/api/content-items/1 -H 'Content-Type: application/json' -H 'Cookie: ...' -d '{"stage":"translate"}'
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add server/server.js
+git commit -m "feat: add ContentItems CRUD API routes"
+```
+
+### Task 3.3: Pipeline Kanban Board
+
+**Files:**
+- Create: `client/src/components/ContentPipelineBoard.jsx`
+- Modify: `client/src/pages/Content.jsx`
+
+- [ ] **Step 1: Create ContentPipelineBoard component**
+
+Uses the existing `KanbanBoard` component with pipeline-specific columns:
+
+```jsx
+import { useLanguage } from '../i18n/LanguageContext'
+import KanbanBoard from './KanbanBoard'
+import StatusBadge from './StatusBadge'
+
+const PIPELINE_COLUMNS = [
+ { id: 'copy', labelKey: 'content.copy', color: '#818cf8' },
+ { id: 'translate', labelKey: 'content.translations', color: '#f59e0b' },
+ { id: 'design', labelKey: 'content.design', color: '#db2777' },
+ { id: 'post', labelKey: 'content.posts', color: '#059669' },
+ { id: 'published', labelKey: 'content.published', color: '#6366f1' },
+]
+
+export default function ContentPipelineBoard({ items, onMove, onSelect }) {
+ const { t } = useLanguage()
+
+ const columns = PIPELINE_COLUMNS.map(col => ({
+ id: col.id,
+ label: t(col.labelKey),
+ color: col.color,
+ }))
+
+ const renderCard = (item) => (
+ onSelect(item)}
+ className="bg-surface-primary border border-border rounded-lg p-3 cursor-pointer card-hover"
+ >
+
{item.title}
+
+ {item.brand_name && (
+
+ {item.brand_name}
+
+ )}
+ {item.assignee_name && (
+ {item.assignee_name}
+ )}
+
+
+ )
+
+ return (
+ item.id || item.Id}
+ onMove={(itemId, newStage) => onMove(itemId, newStage)}
+ emptyLabel={t('content.emptyStage')}
+ />
+ )
+}
+```
+
+**IMPORTANT: KanbanBoard compatibility fix needed.** KanbanBoard hardcodes `item.status` (line 38, 47, 48) and expects `col.color` as a Tailwind class (e.g., `bg-indigo-400`), not a hex color. Two fixes:
+
+**Fix A — Add `statusField` prop to KanbanBoard.jsx** (preferred, backwards-compatible):
+```jsx
+// Change signature:
+export default function KanbanBoard({ columns, items, renderCard, getItemId, onMove, emptyLabel, statusField = 'status' })
+
+// Change line 38: draggedItem.status → draggedItem[statusField]
+// Change line 47: item.status === col.id → item[statusField] === col.id
+// Change line 48: draggedItem?.status !== col.id → draggedItem?.[statusField] !== col.id
+```
+
+Then pass `statusField="stage"` from ContentPipelineBoard.
+
+**Fix B — Use Tailwind classes for column colors** (not hex):
+```jsx
+const PIPELINE_COLUMNS = [
+ { id: 'copy', labelKey: 'content.copy', color: 'bg-indigo-400' },
+ { id: 'translate', labelKey: 'content.translations', color: 'bg-amber-400' },
+ { id: 'design', labelKey: 'content.design', color: 'bg-pink-500' },
+ { id: 'post', labelKey: 'content.posts', color: 'bg-emerald-500' },
+ { id: 'published', labelKey: 'content.published', color: 'bg-violet-500' },
+]
+```
+
+- [ ] **Step 2: Wire PipelineTab in Content.jsx**
+
+```jsx
+import ContentPipelineBoard from '../components/ContentPipelineBoard'
+import api from '../utils/api'
+
+function PipelineTab() {
+ const [items, setItems] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ useEffect(() => {
+ api.get('/content-items').then(data => {
+ setItems(data)
+ setLoading(false)
+ })
+ }, [])
+
+ const handleMove = async (itemId, newStage) => {
+ // Optimistic update
+ setItems(prev => prev.map(i =>
+ (i.id || i.Id) === itemId ? { ...i, stage: newStage } : i
+ ))
+ await api.patch(`/content-items/${itemId}`, { stage: newStage })
+ }
+
+ if (loading) return
+
+ return {}} />
+}
+```
+
+- [ ] **Step 3: Add i18n keys**
+
+```json
+"content.translate": "Translate",
+"content.post": "Post",
+"content.published": "Published",
+"content.emptyStage": "No items at this stage"
+```
+
+- [ ] **Step 4: Test pipeline kanban renders and drag works**
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/components/ContentPipelineBoard.jsx client/src/pages/Content.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: pipeline kanban board for Content page"
+```
+
+### Task 3.4: Content Item Detail Panel
+
+**Files:**
+- Create: `client/src/components/ContentDetailPanel.jsx`
+- Modify: `client/src/pages/Content.jsx`
+
+- [ ] **Step 1: Create ContentDetailPanel**
+
+Panel showing pipeline breadcrumb + linked items per stage:
+
+```jsx
+import SlidePanel from './SlidePanel'
+import OverflowMenu from './OverflowMenu'
+import { ArrowLeft, Check, Circle, Clock } from 'lucide-react'
+import { useLanguage } from '../i18n/LanguageContext'
+
+// Stage keys match i18n: content.copy, content.translate, content.design, content.post, content.published
+const STAGES = ['copy', 'translate', 'design', 'post', 'published']
+
+function StageBreadcrumb({ currentStage }) {
+ const { t } = useLanguage()
+ return (
+
+ {STAGES.map((stage, i) => {
+ const stageIndex = STAGES.indexOf(currentStage)
+ const isDone = i < stageIndex
+ const isCurrent = i === stageIndex
+ return (
+
+ {i > 0 && → }
+
+ {isDone ? : isCurrent ? : }
+ {t(`content.${stage}`)}
+
+
+ )
+ })}
+
+ )
+}
+
+export default function ContentDetailPanel({ item, onClose, onSave, onDelete }) {
+ const { t } = useLanguage()
+
+ if (!item) return null
+
+ const header = (
+
+
+
+
+
{item.title}
+
+ {t('common.save')}
+
+
+
+ )
+
+ return (
+
+
+
+ {/* Title edit */}
+
+ {t('common.title')}
+
+
+ {/* Campaign + Brand + Assignee selectors would go here */}
+ {/* Linked items per stage would go here — Phase 3 continued */}
+
+
+ )
+}
+```
+
+- [ ] **Step 2: Wire into Content.jsx PipelineTab**
+
+Add state for `selectedItem` and render `ContentDetailPanel` when an item is selected.
+
+- [ ] **Step 3: Test panel opens on card click with breadcrumb**
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/components/ContentDetailPanel.jsx client/src/pages/Content.jsx
+git commit -m "feat: content detail panel with pipeline stage breadcrumb"
+```
+
+---
+
+## Chunk 4: Phase 4 — Campaign Brief Enhancement
+
+### Task 4.1: Server — Campaign Brief Columns
+
+**Files:**
+- Modify: `server/server.js`
+
+- [ ] **Step 1: Add campaign brief columns to TEXT_COLUMNS**
+
+```js
+Campaigns: [
+ { name: 'target_audience', uidt: 'LongText' },
+ { name: 'key_messages', uidt: 'LongText' },
+ { name: 'reach_target', uidt: 'Number' },
+ { name: 'engagement_target', uidt: 'Number' },
+ { name: 'conversion_target', uidt: 'Number' },
+ { name: 'approval_status', uidt: 'SingleLineText' },
+ { name: 'approved_by', uidt: 'SingleLineText' },
+ { name: 'approved_at', uidt: 'SingleLineText' },
+],
+```
+
+Note: **Skip `goals`** — it already exists as `LongText` in `REQUIRED_TABLES` (line 209). It stores JSON-stringified arrays and is already parsed client-side in `api.js`. Reuse it for the goal tags (store as `["awareness","engagement"]`).
+
+- [ ] **Step 2: Update PATCH /api/campaigns to handle new fields**
+
+Add the new fields to the allowed update fields list in the PATCH route.
+
+- [ ] **Step 3: Add campaign approval endpoint**
+
+```js
+app.post('/api/campaigns/:id/approve', requireAuth, async (req, res) => {
+ try {
+ if (req.session.userRole !== 'superadmin' && req.session.userRole !== 'manager') {
+ return res.status(403).json({ error: 'Only managers can approve campaigns' })
+ }
+ await nocodb.update('Campaigns', req.params.id, {
+ approval_status: 'approved',
+ approved_by: String(req.session.userId),
+ approved_at: new Date().toISOString(),
+ })
+ const updated = await nocodb.get('Campaigns', req.params.id)
+ res.json(updated)
+ } catch (err) {
+ res.status(500).json({ error: 'Failed to approve campaign' })
+ }
+})
+```
+
+- [ ] **Step 4: Restart server, verify columns created**
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add server/server.js
+git commit -m "feat: campaign brief columns + approval endpoint"
+```
+
+### Task 4.2: Campaign Detail — Brief Section UI
+
+**Files:**
+- Modify: `client/src/pages/CampaignDetail.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Read CampaignDetail.jsx**
+
+- [ ] **Step 2: Add Brief section to campaign detail**
+
+Above or below the existing timeline, add a "Brief" section with:
+- Goals: tag/chip selector from predefined list (awareness, engagement, conversions, brand_building, lead_generation)
+- Target audience: textarea
+- Key messages: textarea
+- Metrics targets: 3 number inputs (reach, engagement rate, conversions)
+- Approval status badge + "Request Approval" / "Approve" button
+
+- [ ] **Step 3: Add "Content Items" section**
+
+Below the brief, show linked content items as small cards with pipeline stage badges. Include "Create Content" button (only visible when `approval_status === 'approved'`).
+
+- [ ] **Step 4: Add i18n keys for all new labels**
+
+- [ ] **Step 5: Test campaign brief editing and approval flow**
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add client/src/pages/CampaignDetail.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: campaign brief section with goals, metrics, approval gate"
+```
+
+---
+
+## Chunk 5: Phase 5 — Dashboard Redesign
+
+### Task 5.1: Redesign Dashboard Layout
+
+**Files:**
+- Modify: `client/src/pages/Dashboard.jsx`
+- Modify: `client/src/i18n/en.json`
+- Modify: `client/src/i18n/ar.json`
+
+- [ ] **Step 1: Read Dashboard.jsx fully**
+
+Understand all existing widgets, data fetching, and state.
+
+- [ ] **Step 2: Restructure to new layout**
+
+Replace the existing multi-grid layout with:
+
+```
+Top row: 4 metric cards (active campaigns, content in pipeline, awaiting approval, published)
+Middle left: Pipeline funnel (horizontal bars per stage)
+Middle right: My Tasks (assigned items needing action)
+Bottom left: Recent Activity feed
+Bottom right: Upcoming Deadlines
+```
+
+- [ ] **Step 3: Build metric cards**
+
+4 cards using the existing `StatCard` component or a new variant:
+- Active Campaigns: `GET /api/campaigns?status=active` count
+- Content in Pipeline: `GET /api/content-items` count, grouped by stage
+- Awaiting Your Approval: count of items where current user is approver and status is pending
+- Published This Period: count of posts with status=published in date range
+
+- [ ] **Step 4: Build Pipeline Funnel widget**
+
+Horizontal stacked bars showing count per stage. Each bar is clickable → navigates to `/content/{stage}`.
+
+```jsx
+function PipelineFunnel({ stageCounts }) {
+ const maxCount = Math.max(...Object.values(stageCounts), 1)
+ return (
+
+ )
+}
+```
+
+- [ ] **Step 5: Build My Tasks widget**
+
+Aggregates: content items assigned to user, pending approvals, tasks, issues. Show as a simple list with type icon + title + urgency indicator.
+
+- [ ] **Step 6: Build Recent Activity widget**
+
+There is no `/api/activity` endpoint. Compose the feed client-side from recent items across entities:
+- Fetch recent posts (sorted by UpdatedAt, limit 5)
+- Fetch recent content items (sorted by UpdatedAt, limit 5)
+- Fetch recent issues (sorted by updated_at, limit 5)
+- Merge, sort by date descending, take top 10
+- Display: avatar + "User updated Post Title" + relative timestamp
+
+Alternative (simpler): Skip this widget for now and add a `GET /api/activity` server endpoint later. Show a placeholder "Activity feed — coming soon" instead. **Ask the user which approach they prefer during implementation.**
+
+- [ ] **Step 7: Build Upcoming Deadlines widget**
+
+Items with due dates approaching. Color-coded: red (overdue), amber (this week), gray (later).
+
+- [ ] **Step 8: Add i18n keys**
+
+```json
+"dashboard.activeCampaigns": "Active Campaigns",
+"dashboard.contentInPipeline": "Content in Pipeline",
+"dashboard.awaitingApproval": "Awaiting Your Approval",
+"dashboard.publishedThisPeriod": "Published This Period",
+"dashboard.pipelineOverview": "Pipeline Overview",
+"dashboard.myTasks": "My Tasks",
+"dashboard.recentActivity": "Recent Activity",
+"dashboard.upcomingDeadlines": "Upcoming Deadlines"
+```
+
+(+ Arabic equivalents)
+
+- [ ] **Step 9: Test dashboard loads with all widgets**
+
+- [ ] **Step 10: Commit**
+
+```bash
+git add client/src/pages/Dashboard.jsx client/src/i18n/en.json client/src/i18n/ar.json
+git commit -m "feat: redesign dashboard — pipeline funnel, my tasks, activity feed, deadlines"
+```
+
+---
+
+## Chunk 6: Phase 6 — Premium Polish
+
+### Task 6.1: Enhanced Animations & Transitions
+
+**Files:**
+- Modify: `client/src/index.css`
+
+- [ ] **Step 1: Add spring-like bezier curves**
+
+```css
+:root {
+ --spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
+ --spring-smooth: cubic-bezier(0.22, 1, 0.36, 1);
+}
+```
+
+- [ ] **Step 2: Add new keyframes**
+
+```css
+@keyframes slideInRight {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+@keyframes countUp {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes barGrow {
+ from { width: 0; }
+}
+
+@keyframes fadeSlideUp {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+```
+
+- [ ] **Step 3: Add prefers-reduced-motion override**
+
+```css
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+```
+
+- [ ] **Step 4: Enhance existing card-hover**
+
+```css
+.card-hover {
+ transition: transform 0.2s var(--spring-smooth), box-shadow 0.2s ease;
+}
+.card-hover:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px -5px rgba(0,0,0,0.1), 0 0 0 1px rgba(79,70,229,0.08);
+}
+```
+
+- [ ] **Step 5: Add button press effect**
+
+```css
+.btn-press {
+ transition: transform 0.1s ease;
+}
+.btn-press:hover { transform: scale(1.02); }
+.btn-press:active { transform: scale(0.98); }
+```
+
+- [ ] **Step 6: Add tab indicator slide**
+
+```css
+.tab-indicator {
+ transition: left 0.3s var(--spring-bounce), width 0.3s var(--spring-bounce);
+}
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add client/src/index.css
+git commit -m "feat: premium animations — spring curves, card hover, button press, tab slide"
+```
+
+### Task 6.2: Glass Effects & Visual Depth
+
+**Files:**
+- Modify: `client/src/index.css`
+- Modify: `client/src/components/SlidePanel.jsx`
+- Modify: `client/src/components/Modal.jsx`
+- Modify: `client/src/components/StatusBadge.jsx`
+
+- [ ] **Step 1: SlidePanel — frosted glass header**
+
+Update SlidePanel so the header section has:
+```css
+backdrop-filter: blur(20px);
+background: rgba(255, 255, 255, 0.85);
+```
+(Dark mode: `rgba(21, 21, 30, 0.85)`)
+
+Make header sticky (`sticky top-0 z-10`) so it stays visible while content scrolls.
+
+- [ ] **Step 2: Modal — deeper backdrop blur**
+
+Update Modal backdrop from `backdrop-blur-sm` to `backdrop-blur-md` (8px → 12px).
+
+- [ ] **Step 3: StatusBadge — glass morphism**
+
+Add a `.badge-glass` variant class:
+```css
+.badge-glass {
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+}
+```
+
+Apply to StatusBadge in dark mode.
+
+- [ ] **Step 4: Add layered shadows for depth**
+
+```css
+.shadow-layered {
+ box-shadow:
+ 0 1px 2px rgba(0,0,0,0.04),
+ 0 4px 8px rgba(0,0,0,0.04),
+ 0 16px 32px rgba(0,0,0,0.04);
+}
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add client/src/index.css client/src/components/SlidePanel.jsx client/src/components/Modal.jsx client/src/components/StatusBadge.jsx
+git commit -m "feat: glass effects — frosted panel header, deeper blur, layered shadows"
+```
+
+### Task 6.3: Typography & Spacing Refinement
+
+**Files:**
+- Modify: `client/src/index.css`
+- Modify: `client/src/components/PageHeader.jsx`
+
+- [ ] **Step 1: Update page title typography**
+
+In PageHeader, update the h1:
+```jsx
+{title}
+```
+
+- [ ] **Step 2: Add tabular-nums utility**
+
+In index.css:
+```css
+.tabular-nums { font-variant-numeric: tabular-nums; }
+```
+
+Apply to all metric/count displays in Dashboard, KanbanBoard column counts, etc.
+
+- [ ] **Step 3: Enforce text-text-secondary for all secondary text**
+
+Search for any raw `text-gray-*` classes used for secondary text and replace with `text-text-secondary`. This ensures dark mode consistency.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/index.css client/src/components/PageHeader.jsx
+git commit -m "feat: typography refinement — lighter titles, tabular nums, consistent text colors"
+```
+
+### Task 6.4: Premium Empty States
+
+**Files:**
+- Modify: `client/src/components/EmptyState.jsx`
+
+- [ ] **Step 1: Enhance EmptyState with gradient icon background**
+
+Update the icon container in the full (non-compact) variant:
+
+```jsx
+
+
+
+```
+
+- [ ] **Step 2: Update copy tone**
+
+Add a `friendlyCopy` prop or update existing description strings across all pages to use warmer language. Example: "No artefacts found" → "No designs yet — create your first one to get started."
+
+- [ ] **Step 3: Add subtle entrance animation**
+
+```jsx
+
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/components/EmptyState.jsx
+git commit -m "feat: premium empty states — gradient icon, warmer copy, entrance animation"
+```
+
+### Task 6.5: Dashboard Widget Animations
+
+**Files:**
+- Modify: `client/src/pages/Dashboard.jsx`
+
+- [ ] **Step 1: Add count-up animation to metric cards**
+
+Use a simple `useCountUp` hook:
+```jsx
+function useCountUp(target, duration = 800) {
+ const [value, setValue] = useState(0)
+ useEffect(() => {
+ if (!target) return
+ const start = Date.now()
+ const tick = () => {
+ const progress = Math.min((Date.now() - start) / duration, 1)
+ setValue(Math.round(progress * target))
+ if (progress < 1) requestAnimationFrame(tick)
+ }
+ requestAnimationFrame(tick)
+ }, [target, duration])
+ return value
+}
+```
+
+- [ ] **Step 2: Add staggered fade-in to activity feed items**
+
+```jsx
+{activities.map((item, i) => (
+
+ ...
+
+))}
+```
+
+- [ ] **Step 3: Add bar grow animation to pipeline funnel**
+
+```jsx
+
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add client/src/pages/Dashboard.jsx
+git commit -m "feat: dashboard widget animations — count-up, staggered feed, bar grow"
+```
+
+---
+
+## Execution Notes
+
+- **Phase order matters loosely:** Phase 1 should go first (nav reorg creates the Content route). Phases 2-5 can be done in any order after Phase 1. Phase 6 (polish) should go last.
+- **Each commit is independently deployable** — the app works after every commit.
+- **Test after every task** — run `npm run dev` and verify the changes visually.
+- **NocoDB columns auto-create on restart** — after any server.js schema change, restart the server and check logs.
+- **i18n:** Always add keys to both `en.json` and `ar.json` in the same commit.