diff --git a/docs/superpowers/plans/2026-04-09-saas-ux-polish-plan.md b/docs/superpowers/plans/2026-04-09-saas-ux-polish-plan.md
new file mode 100644
index 0000000..459120d
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-09-saas-ux-polish-plan.md
@@ -0,0 +1,760 @@
+# SaaS Platform UX Polish — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix layout bugs, replace hardcoded dark-only colors with design system tokens, improve navigation/header, add error handling, and adopt design system components consistently across the SaaS platform UI.
+
+**Architecture:** All changes are in the existing SaaS platform UI (`ui/src/`) and sign-in page (`ui/sign-in/src/`). The platform uses `@cameleer/design-system` components and Tailwind CSS. The key issue is that pages use hardcoded `text-white` Tailwind classes instead of DS CSS variables, and the DS `TopBar` renders server-specific controls that are irrelevant on platform pages.
+
+**Tech Stack:** React 19, TypeScript, Tailwind CSS, `@cameleer/design-system`, React Router v6, Logto SDK
+
+**Spec:** `docs/superpowers/specs/2026-04-09-saas-ux-polish-design.md`
+
+---
+
+## Task 1: Fix label/value collision and replace hardcoded colors
+
+**Spec items:** 1.1, 1.2
+
+**Files:**
+- Create: `ui/src/styles/platform.module.css`
+- Modify: `ui/src/pages/DashboardPage.tsx`
+- Modify: `ui/src/pages/LicensePage.tsx`
+- Modify: `ui/src/pages/AdminTenantsPage.tsx`
+
+- [ ] **Step 1: Create shared platform CSS module**
+
+Create `ui/src/styles/platform.module.css` with DS-variable-based classes replacing the hardcoded Tailwind colors:
+
+```css
+.heading {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.textPrimary {
+ color: var(--text-primary);
+}
+
+.textSecondary {
+ color: var(--text-secondary);
+}
+
+.textMuted {
+ color: var(--text-muted);
+}
+
+.mono {
+ font-family: var(--font-mono);
+}
+
+.kvRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.kvLabel {
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+.kvValue {
+ font-size: 0.875rem;
+ color: var(--text-primary);
+}
+
+.kvValueMono {
+ font-size: 0.875rem;
+ color: var(--text-primary);
+ font-family: var(--font-mono);
+}
+
+.dividerList {
+ display: flex;
+ flex-direction: column;
+}
+
+.dividerList > * + * {
+ border-top: 1px solid var(--border-subtle);
+}
+
+.dividerRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem 0;
+}
+
+.dividerRow:first-child {
+ padding-top: 0;
+}
+
+.dividerRow:last-child {
+ padding-bottom: 0;
+}
+
+.description {
+ font-size: 0.875rem;
+ color: var(--text-muted);
+}
+
+.tokenBlock {
+ margin-top: 0.5rem;
+ border-radius: var(--radius-sm);
+ background: var(--bg-inset);
+ border: 1px solid var(--border-subtle);
+ padding: 0.75rem;
+ overflow-x: auto;
+}
+
+.tokenCode {
+ font-size: 0.75rem;
+ font-family: var(--font-mono);
+ color: var(--text-secondary);
+ word-break: break-all;
+}
+```
+
+- [ ] **Step 2: Update DashboardPage to use CSS module + fix label/value**
+
+In `ui/src/pages/DashboardPage.tsx`:
+
+1. Add import:
+```typescript
+import s from '../styles/platform.module.css';
+```
+
+2. Replace all hardcoded color classes:
+- Line 71: `text-2xl font-semibold text-white` → `className={s.heading}`
+- Lines 96, 100, 107: `className="flex justify-between text-white/80"` → `className={s.kvRow}`
+- Inner label spans: wrap with `className={s.kvLabel}`
+- Inner value spans: wrap with `className={s.kvValueMono}` (for mono) or `className={s.kvValue}`
+- Line 116: `text-sm text-white/60` → `className={s.description}`
+
+3. The label/value collision fix: the `kvRow` class uses explicit `display: flex; width: 100%; justify-content: space-between` which ensures the flex container stretches to full Card width regardless of Card's inner layout.
+
+- [ ] **Step 3: Update LicensePage to use CSS module**
+
+In `ui/src/pages/LicensePage.tsx`:
+
+1. Add import: `import s from '../styles/platform.module.css';`
+
+2. Replace all hardcoded color classes:
+- Line 85: heading → `className={s.heading}`
+- Lines 95-115 (Validity rows): `flex items-center justify-between` → `className={s.kvRow}`, labels → `className={s.kvLabel}`, values → `className={s.kvValue}`
+- Lines 121-136 (Features): `divide-y divide-white/10` → `className={s.dividerList}`, rows → `className={s.dividerRow}`, feature name `text-sm text-white` → `className={s.textPrimary}` + `text-sm`
+- Lines 142-157 (Limits): same dividerList/dividerRow pattern, label → `className={s.kvLabel}`, value → `className={s.kvValueMono}`
+- Line 163: description text → `className={s.description}`
+- Lines 174-178: token code block → `className={s.tokenBlock}` on outer div, `className={s.tokenCode}` on code element
+
+- [ ] **Step 4: Update AdminTenantsPage to use CSS module**
+
+In `ui/src/pages/AdminTenantsPage.tsx`:
+- Line 62: `text-2xl font-semibold text-white` → `className={s.heading}`
+
+- [ ] **Step 5: Verify in both themes**
+
+1. Open the platform dashboard in browser
+2. Check label/value pairs have proper spacing (Slug on left, "default" on right)
+3. Toggle to light theme via TopBar toggle
+4. Verify all text is readable in light mode (no invisible white-on-white)
+5. Toggle back to dark mode — should look the same as before
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add ui/src/styles/platform.module.css ui/src/pages/DashboardPage.tsx ui/src/pages/LicensePage.tsx ui/src/pages/AdminTenantsPage.tsx
+git commit -m "fix: replace hardcoded text-white with DS variables, fix label/value layout"
+```
+
+---
+
+## Task 2: Remove redundant dashboard elements
+
+**Spec items:** 1.3, 2.4
+
+**Files:**
+- Modify: `ui/src/pages/DashboardPage.tsx`
+
+- [ ] **Step 1: Remove primary "Open Server Dashboard" button from header**
+
+In `ui/src/pages/DashboardPage.tsx`, find the header area (lines ~75-88). Remove the primary Button for "Open Server Dashboard" (lines ~81-87). Keep:
+- The Server Management Card with its secondary button (lines ~113-126)
+- The sidebar footer link (in Layout.tsx — don't touch)
+
+The header area should just have the tenant name heading + tier badge, no button.
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add ui/src/pages/DashboardPage.tsx
+git commit -m "fix: remove redundant Open Server Dashboard button from dashboard header"
+```
+
+---
+
+## Task 3: Fix header controls and sidebar navigation
+
+**Spec items:** 2.1, 2.2, 2.3, 2.5
+
+**Files:**
+- Modify: `ui/src/components/Layout.tsx`
+- Modify: `ui/src/main.tsx` (possibly)
+
+- [ ] **Step 1: Investigate TopBar props for hiding controls**
+
+The DS `TopBar` interface (from types):
+```typescript
+interface TopBarProps {
+ breadcrumb: BreadcrumbItem[];
+ environment?: ReactNode;
+ user?: { name: string };
+ onLogout?: () => void;
+ className?: string;
+}
+```
+
+The TopBar has NO props to hide status filters, time range, auto-refresh, or search. These are hardcoded inside the component.
+
+**Options:**
+1. Check if removing `GlobalFilterProvider` and `CommandPaletteProvider` from `main.tsx` makes TopBar gracefully hide those sections (test this first)
+2. If that causes errors, add `display: none` CSS overrides for the irrelevant sections
+3. If neither works, build a simplified platform header
+
+Try option 1 first. In `main.tsx`, remove `GlobalFilterProvider` and `CommandPaletteProvider` from the provider stack. Test if the app still renders. If TopBar crashes without them, revert and try option 2.
+
+- [ ] **Step 2: Add sidebar active state**
+
+In `ui/src/components/Layout.tsx`, add route-based active state:
+
+```typescript
+import { useLocation } from 'react-router';
+
+// Inside the Layout component:
+const location = useLocation();
+```
+
+Update each `Sidebar.Section`:
+```tsx
+}
+ label="Dashboard"
+ open={false}
+ active={location.pathname === '/' || location.pathname === ''}
+ onToggle={() => navigate('/')}
+>
+ {null}
+
+
+}
+ label="License"
+ open={false}
+ active={location.pathname === '/license'}
+ onToggle={() => navigate('/license')}
+>
+ {null}
+
+
+{scopes.has('platform:admin') && (
+ }
+ label="Platform"
+ open={false}
+ active={location.pathname.startsWith('/admin')}
+ onToggle={() => navigate('/admin/tenants')}
+ >
+ {null}
+
+)}
+```
+
+- [ ] **Step 3: Add breadcrumbs**
+
+In Layout.tsx, compute breadcrumbs from the current route:
+
+```typescript
+const breadcrumb = useMemo((): BreadcrumbItem[] => {
+ const path = location.pathname;
+ if (path.startsWith('/admin')) return [{ label: 'Admin' }, { label: 'Tenants' }];
+ if (path.startsWith('/license')) return [{ label: 'License' }];
+ return [{ label: 'Dashboard' }];
+}, [location.pathname]);
+```
+
+Pass to TopBar:
+```tsx
+
+```
+
+Import `BreadcrumbItem` type from `@cameleer/design-system` if needed.
+
+- [ ] **Step 4: Fix sidebar collapse**
+
+Replace the hardcoded collapse state:
+```typescript
+const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+```
+
+```tsx
+ setSidebarCollapsed(c => !c)}>
+```
+
+- [ ] **Step 5: Fix username null fallback**
+
+Update the user prop (line ~125):
+```tsx
+const displayName = username || 'User';
+
+
+```
+
+This ensures the logout button is always visible.
+
+- [ ] **Step 6: Replace custom SVG icons with lucide-react**
+
+Replace the 4 custom SVG icon components (lines 25-62) with lucide-react icons:
+
+```typescript
+import { LayoutDashboard, ShieldCheck, Building, Server } from 'lucide-react';
+```
+
+Then update sidebar sections:
+```tsx
+icon={} // was
+icon={} // was
+icon={} // was
+```
+
+Remove the 4 custom SVG component functions (DashboardIcon, LicenseIcon, ObsIcon, PlatformIcon).
+
+- [ ] **Step 7: Verify**
+
+1. Sidebar shows active highlight on current page
+2. Breadcrumbs show "Dashboard", "License", or "Admin > Tenants"
+3. Sidebar collapse works (click collapse button, sidebar minimizes)
+4. User avatar/logout always visible
+5. Icons render correctly from lucide-react
+6. Check if server controls are hidden (depending on step 1 result)
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add ui/src/components/Layout.tsx ui/src/main.tsx
+git commit -m "fix: sidebar active state, breadcrumbs, collapse, username fallback, lucide icons"
+```
+
+---
+
+## Task 4: Error handling and OrgResolver fix
+
+**Spec items:** 3.1, 3.2, 3.7
+
+**Files:**
+- Modify: `ui/src/auth/OrgResolver.tsx`
+- Modify: `ui/src/pages/DashboardPage.tsx`
+- Modify: `ui/src/pages/AdminTenantsPage.tsx`
+
+- [ ] **Step 1: Fix OrgResolver error state**
+
+In `ui/src/auth/OrgResolver.tsx`, find the error handling (lines 88-90):
+
+```tsx
+// BEFORE:
+if (isError) {
+ return null;
+}
+
+// AFTER:
+if (isError) {
+ return (
+
+
+
+
+ );
+}
+```
+
+Add imports: `EmptyState`, `Button` from `@cameleer/design-system`. Ensure `refetch` is available from the query hook (check if `useQuery` returns it).
+
+- [ ] **Step 2: Add error handling to DashboardPage**
+
+In `ui/src/pages/DashboardPage.tsx`, after the loading check (line ~49) and tenant check (line ~57), add error handling:
+
+```tsx
+const { data: tenant, isError: tenantError } = useTenant();
+const { data: license, isError: licenseError } = useLicense();
+
+// After loading spinner check:
+if (tenantError || licenseError) {
+ return (
+
+
+
+ );
+}
+```
+
+Check how `useTenant()` and `useLicense()` expose error state — they may use `isError` from React Query.
+
+- [ ] **Step 3: Add empty state and date formatting to AdminTenantsPage**
+
+In `ui/src/pages/AdminTenantsPage.tsx`:
+
+1. Add error handling:
+```tsx
+if (isError) {
+ return (
+
+
+
+ );
+}
+```
+
+2. Format the `createdAt` column (line 31):
+```tsx
+// BEFORE:
+{ key: 'createdAt', header: 'Created' },
+
+// AFTER:
+{ key: 'createdAt', header: 'Created', render: (_, row) => new Date(row.createdAt).toLocaleDateString() },
+```
+
+3. Add empty state to DataTable (if supported) or show EmptyState when tenants is empty:
+```tsx
+{(!tenants || tenants.length === 0) ? (
+
+) : (
+
+)}
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add ui/src/auth/OrgResolver.tsx ui/src/pages/DashboardPage.tsx ui/src/pages/AdminTenantsPage.tsx
+git commit -m "fix: add error states to OrgResolver, DashboardPage, AdminTenantsPage"
+```
+
+---
+
+## Task 5: DS component adoption and license token copy
+
+**Spec items:** 3.3, 3.4
+
+**Files:**
+- Modify: `ui/src/pages/LicensePage.tsx`
+
+- [ ] **Step 1: Replace raw button with DS Button**
+
+In `ui/src/pages/LicensePage.tsx`, find the token toggle button (lines ~166-172):
+
+```tsx
+// BEFORE:
+
+
+// AFTER:
+
+```
+
+Ensure `Button` is imported from `@cameleer/design-system`.
+
+- [ ] **Step 2: Add copy-to-clipboard button**
+
+Add `useToast` import and `Copy` icon:
+```typescript
+import { useToast } from '@cameleer/design-system';
+import { Copy } from 'lucide-react';
+```
+
+Add toast hook in component:
+```typescript
+const { toast } = useToast();
+```
+
+Next to the show/hide button, add a copy button (only when expanded):
+```tsx
+