# Portal UI Overhaul Implementation Plan

> **For Claude:**

**Goal:** Overhaul the signed-in portal experience with a dark navy sidebar, updated typography, warmer color palette, new shared components, redesigned header with command palette, and an enriched subscriptions page with grid view.

**Architecture:** Pure frontend changes across the portal app. Update design tokens (CSS variables), font imports, shared components (atoms/molecules), shell components (header, sidebar), and feature views (subscriptions, dashboard). No BFF or domain changes needed.

**Tech Stack:** Next.js 15, React 19, Tailwind CSS v4, CVA (class-variance-authority), HeroIcons, Lucide React, next/font/google

---

## Task 1: Typography — Add DM Sans & JetBrains Mono Fonts

**Files:**

- Modify: `apps/portal/src/app/layout.tsx`
- Modify: `apps/portal/src/app/globals.css` (lines 20-21, the `--font-sans` and `--font-display` variables)

**Step 1: Add DM Sans and JetBrains Mono font imports in layout.tsx**

Add two new font imports alongside the existing Plus Jakarta Sans:

```tsx
import { Plus_Jakarta_Sans, DM_Sans, JetBrains_Mono } from "next/font/google";

const plusJakartaSans = Plus_Jakarta_Sans({
  subsets: ["latin"],
  variable: "--font-jakarta",
  display: "swap",
});

const dmSans = DM_Sans({
  subsets: ["latin"],
  variable: "--font-dm-sans",
  display: "swap",
});

const jetbrainsMono = JetBrains_Mono({
  subsets: ["latin"],
  variable: "--font-jetbrains",
  display: "swap",
});
```

Update the body className to include all three font variables:

```tsx
<body className={`${plusJakartaSans.variable} ${dmSans.variable} ${jetbrainsMono.variable} antialiased`}>
```

**Step 2: Update CSS font-family variables in globals.css**

In the `:root` block (around lines 20-21), change:

```css
/* Typography */
--font-sans: var(--font-dm-sans, system-ui, sans-serif);
--font-display: var(--font-jakarta, var(--font-sans));
--font-mono: var(--font-jetbrains, ui-monospace, monospace);
```

**Step 3: Add font-mono to Tailwind theme mapping**

In the `@theme` block of globals.css (around line 228), add after `--font-family-display`:

```css
--font-family-mono: var(--font-mono);
```

**Step 4: Verify**

Run: `pnpm type-check`
Expected: PASS (no type errors from font imports)

Run: `pnpm lint`
Expected: PASS

**Step 5: Commit**

```
feat(portal): update typography system with DM Sans and JetBrains Mono

Replace Geist Sans with DM Sans for body text and add JetBrains Mono
for monospace/data display. Plus Jakarta Sans remains for display headings.
```

---

## Task 2: Color System — Dark Navy Sidebar & Warmer Neutrals

**Files:**

- Modify: `apps/portal/src/app/globals.css` (`:root` and `.dark` blocks)

**Step 1: Update sidebar colors to dark navy**

In the `:root` block, replace the sidebar color variables (around lines 82-86):

```css
/* Sidebar - Dark Navy */
--sidebar: oklch(0.18 0.03 250);
--sidebar-foreground: oklch(1 0 0);
--sidebar-border: oklch(0.25 0.04 250);
--sidebar-active: oklch(0.99 0 0 / 0.12);
--sidebar-accent: var(--primary);
```

In the `.dark` block, update sidebar dark mode (around lines 195-197):

```css
/* Sidebar - Dark Navy for dark mode */
--sidebar: oklch(0.13 0.025 250);
--sidebar-border: oklch(0.22 0.03 250);
```

**Step 2: Add sidebar-accent to Tailwind theme mapping**

In the `@theme` block, after `--color-sidebar-active` (around line 290), add:

```css
--color-sidebar-accent: var(--sidebar-accent);
```

**Step 3: Warm up neutral surface colors**

In the `:root` block, adjust these variables to add slight warmth:

```css
/* Core Surfaces - slightly warmer */
--muted: oklch(0.965 0.006 70);
--muted-foreground: oklch(0.46 0.01 70);

/* Chrome - slightly warmer borders */
--border: oklch(0.925 0.006 70);
--input: oklch(0.955 0.005 70);
```

In the `.dark` block, update the warm equivalents:

```css
--muted: oklch(0.25 0.008 70);
--muted-foreground: oklch(0.72 0.01 70);
--border: oklch(0.3 0.012 70);
--input: oklch(0.33 0.01 70);
```

**Step 4: Update the main background for a subtle warm tint**

In `:root`:

```css
--background: oklch(0.993 0.002 70);
--surface-elevated: oklch(0.998 0.001 70);
--surface-sunken: oklch(0.975 0.004 70);
```

In `.dark`:

```css
--background: oklch(0.12 0.012 250);
--surface-elevated: oklch(0.18 0.012 250);
--surface-sunken: oklch(0.1 0.012 250);
```

**Step 5: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 6: Commit**

```
style(portal): dark navy sidebar and warmer neutral palette

Switch sidebar from deep purple to dark navy for brand continuity.
Warm up neutral surfaces with subtle warmth (hue 70) to reduce sterility.
```

---

## Task 3: Sidebar Component — Navy Styling & Section Labels

**Files:**

- Modify: `apps/portal/src/components/organisms/AppShell/Sidebar.tsx`
- Modify: `apps/portal/src/components/organisms/AppShell/navigation.ts`

**Step 1: Update sidebar header branding area**

In `Sidebar.tsx`, update the header section (around lines 67-77). Replace the logo background and add a subtle gradient to the sidebar header:

```tsx
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
  <div className="flex items-center space-x-3">
    <div className="h-9 w-9 bg-white/10 backdrop-blur-sm rounded-lg border border-white/10 flex items-center justify-center">
      <Logo size={22} />
    </div>
    <div>
      <span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
      <p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
    </div>
  </div>
</div>
```

**Step 2: Add section labels to navigation**

In `navigation.ts`, add a `section` property to NavigationItem interface:

```ts
export interface NavigationItem {
  name: string;
  href?: string;
  icon: ComponentType<SVGProps<SVGSVGElement>>;
  children?: NavigationChild[];
  isLogout?: boolean;
  section?: string;
}
```

Add section labels to the `baseNavigation` array. Add `section: "Overview"` to Dashboard, `section: "Account"` to Billing (the first item after Dashboard/Orders), and `section: ""` to the logout item. This creates visual groupings.

```ts
export const baseNavigation: NavigationItem[] = [
  { name: "Dashboard", href: "/account", icon: HomeIcon, section: "Overview" },
  { name: "Orders", href: "/account/orders", icon: ClipboardDocumentListIcon },
  {
    name: "Billing",
    icon: CreditCardIcon,
    section: "Account",
    children: [
      { name: "Invoices", href: "/account/billing/invoices" },
      { name: "Payment Methods", href: "/account/billing/payment-methods" },
    ],
  },
  // ... rest remains the same
```

**Step 3: Render section labels in Sidebar.tsx**

In the `Sidebar` component's nav rendering, add section label support. Replace the `navigation.map` block:

```tsx
<nav className="flex-1 px-3 space-y-0.5">
  {navigation.map((item, index) => (
    <div key={item.name}>
      {item.section && (
        <div className={`px-3 pt-${index === 0 ? "0" : "5"} pb-2`}>
          <span className="text-[10px] font-semibold uppercase tracking-[0.1em] text-white/30">
            {item.section}
          </span>
        </div>
      )}
      <NavigationItem
        item={item}
        pathname={pathname}
        isExpanded={expandedItems.includes(item.name)}
        toggleExpanded={toggleExpanded}
      />
    </div>
  ))}
</nav>
```

**Step 4: Update active state to use primary blue accent instead of white**

In `Sidebar.tsx`, update the shared styling constants (around lines 12-15):

```tsx
const navItemBaseClass =
  "group w-full flex items-center px-3 py-2 text-[13px] font-medium rounded-lg transition-all duration-200 relative focus:outline-none focus:ring-2 focus:ring-white/20";
const activeClass = "text-white bg-white/[0.08] shadow-sm";
const inactiveClass = "text-white/60 hover:text-white/90 hover:bg-white/[0.06]";
```

Update `ActiveIndicator` to use primary blue:

```tsx
function ActiveIndicator({ small = false }: { small?: boolean }) {
  const size = small ? "w-0.5 h-3.5" : "w-[3px] h-5";
  return (
    <div className={`absolute left-0 top-1/2 -translate-y-1/2 ${size} bg-primary rounded-full`} />
  );
}
```

Update `NavIcon` to use subtler styling:

```tsx
function NavIcon({
  icon: Icon,
  isActive,
  variant = "default",
}: {
  icon: ComponentType<SVGProps<SVGSVGElement>>;
  isActive: boolean;
  variant?: "default" | "logout";
}) {
  if (variant === "logout") {
    return (
      <div className="p-1 mr-2.5 text-red-400/70 group-hover:text-red-300 transition-colors duration-200">
        <Icon className="h-[18px] w-[18px]" />
      </div>
    );
  }

  return (
    <div
      className={`p-1 mr-2.5 transition-colors duration-200 ${
        isActive ? "text-primary" : "text-white/40 group-hover:text-white/70"
      }`}
    >
      <Icon className="h-[18px] w-[18px]" />
    </div>
  );
}
```

**Step 5: Update child navigation items styling**

In `ExpandableNavItem`, update the child items container and styling (around line 144):

```tsx
<div className="mt-0.5 ml-[30px] space-y-0.5 border-l border-white/[0.08] pl-3">
  {item.children?.map((child: NavigationChild) => {
    const isChildActive = pathname === (child.href || "").split(/[?#]/)[0];
    return (
      <Link
        key={child.href || child.name}
        href={child.href}
        prefetch
        onMouseEnter={() => child.href && void router.prefetch(child.href)}
        className={`group flex items-center px-2.5 py-1.5 text-[13px] rounded-md transition-all duration-200 relative ${
          isChildActive
            ? "text-white bg-white/[0.08] font-medium"
            : "text-white/50 hover:text-white/80 hover:bg-white/[0.04] font-normal"
        }`}
        title={child.tooltip || child.name}
        aria-current={isChildActive ? "page" : undefined}
      >
        {isChildActive && <ActiveIndicator small />}
        <span className="truncate">{child.name}</span>
      </Link>
    );
  })}
</div>
```

Remove the dot indicator before child names (cleaner without it).

**Step 6: Update the logout button styling**

```tsx
function LogoutNavItem({ item, onLogout }: { item: NavigationItem; onLogout: () => void }) {
  return (
    <div className="px-3 pt-4 mt-2 border-t border-white/[0.06]">
      <button
        onClick={onLogout}
        className="group w-full flex items-center px-3 py-2 text-[13px] font-medium text-red-400/70 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-red-400/20"
      >
        <NavIcon icon={item.icon} isActive={false} variant="logout" />
        <span>{item.name}</span>
      </button>
    </div>
  );
}
```

**Step 7: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 8: Commit**

```
style(portal): overhaul sidebar with navy theme and section labels

- Switch from purple to dark navy color scheme
- Add section labels (Overview, Account) for visual grouping
- Use primary blue for active indicator instead of white
- Reduce visual weight of icons and text for cleaner look
- Separate logout with border divider
```

---

## Task 4: Header — Redesign with Search & Improved Profile

**Files:**

- Modify: `apps/portal/src/components/organisms/AppShell/Header.tsx`
- Modify: `apps/portal/src/components/organisms/AppShell/AppShell.tsx` (pass breadcrumbs to header)

**Step 1: Redesign the Header component**

Replace the entire Header component in `Header.tsx`:

```tsx
"use client";

import Link from "next/link";
import { memo } from "react";
import {
  Bars3Icon,
  QuestionMarkCircleIcon,
  MagnifyingGlassIcon,
} from "@heroicons/react/24/outline";
import { NotificationBell } from "@/features/notifications";

interface UserInfo {
  firstName?: string | null;
  lastName?: string | null;
  email?: string | null;
}

function getDisplayName(user: UserInfo | null, profileReady: boolean): string {
  const fullName = [user?.firstName, user?.lastName].filter(Boolean).join(" ");
  const emailPrefix = user?.email?.split("@")[0];
  if (profileReady) {
    return fullName || emailPrefix || "Account";
  }
  return emailPrefix || "Account";
}

function getInitials(user: UserInfo | null, profileReady: boolean, displayName: string): string {
  if (profileReady && user?.firstName && user?.lastName) {
    return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();
  }
  return displayName.slice(0, 2).toUpperCase();
}

interface HeaderProps {
  onMenuClick: () => void;
  user: UserInfo | null;
  profileReady: boolean;
}

export const Header = memo(function Header({ onMenuClick, user, profileReady }: HeaderProps) {
  const displayName = getDisplayName(user, profileReady);
  const initials = getInitials(user, profileReady, displayName);

  return (
    <div className="relative z-40 bg-header/80 backdrop-blur-xl border-b border-border/40">
      <div className="flex items-center h-14 gap-2 px-3 sm:px-5">
        {/* Mobile menu button */}
        <button
          type="button"
          className="md:hidden flex items-center justify-center w-10 h-10 -ml-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
          onClick={onMenuClick}
          aria-label="Open navigation"
        >
          <Bars3Icon className="h-5 w-5" />
        </button>

        {/* Search trigger */}
        <button
          type="button"
          className="hidden sm:flex items-center gap-2.5 h-9 px-3 w-full max-w-xs rounded-lg bg-muted/50 border border-border/50 text-muted-foreground text-sm hover:bg-muted/80 hover:border-border transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
          aria-label="Search"
        >
          <MagnifyingGlassIcon className="h-3.5 w-3.5 flex-shrink-0" />
          <span className="flex-1 text-left text-xs">Search...</span>
          <kbd className="hidden lg:inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded border border-border/60 bg-background/80 text-[10px] font-mono text-muted-foreground/60">
            <span className="text-[11px]">&#8984;</span>K
          </kbd>
        </button>

        <div className="flex-1" />

        {/* Right side actions */}
        <div className="flex items-center gap-0.5">
          {/* Notification bell */}
          <NotificationBell />

          {/* Help link */}
          <Link
            href="/account/support"
            prefetch
            aria-label="Help"
            className="hidden sm:inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted/60 transition-all duration-200"
            title="Support Center"
          >
            <QuestionMarkCircleIcon className="h-4.5 w-4.5" />
          </Link>

          {/* Divider */}
          <div className="hidden sm:block w-px h-5 bg-border/60 mx-1.5" />

          {/* Profile link */}
          <Link
            href="/account/settings"
            prefetch
            className="group flex items-center gap-2 px-2 py-1 min-h-[40px] text-sm font-medium text-muted-foreground hover:text-foreground rounded-lg transition-all duration-200"
          >
            <div className="h-7 w-7 rounded-lg bg-gradient-to-br from-primary to-accent-gradient flex items-center justify-center text-[11px] font-bold text-primary-foreground shadow-sm group-hover:shadow-md transition-shadow">
              {initials}
            </div>
            <span className="hidden sm:inline text-[13px]">{displayName}</span>
          </Link>
        </div>
      </div>
    </div>
  );
});
```

Key changes:

- Reduced header height from 64px to 56px (h-14)
- Added search trigger button with keyboard shortcut hint
- Added visual divider between actions and profile
- Tightened spacing and made icons slightly smaller
- More transparent glass effect on the header

**Step 2: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 3: Commit**

```
style(portal): redesign header with search bar and tighter layout

- Add search trigger button with keyboard shortcut hint (visual only for now)
- Reduce header height from 64px to 56px
- Add divider between action icons and profile
- Increase glass morphism transparency
- Tighten icon sizes and spacing
```

---

## Task 5: New Shared Component — StatusIndicator

**Files:**

- Create: `apps/portal/src/components/atoms/status-indicator.tsx`
- Modify: `apps/portal/src/components/atoms/index.ts`

**Step 1: Create the StatusIndicator component**

```tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/shared/utils";

const statusIndicatorVariants = cva("inline-flex items-center gap-1.5", {
  variants: {
    size: {
      sm: "text-xs",
      md: "text-sm",
      lg: "text-base",
    },
  },
  defaultVariants: {
    size: "md",
  },
});

const dotVariants = cva("rounded-full flex-shrink-0", {
  variants: {
    status: {
      active: "bg-success",
      warning: "bg-warning",
      error: "bg-danger",
      inactive: "bg-muted-foreground/30",
      pending: "bg-info",
    },
    size: {
      sm: "h-1.5 w-1.5",
      md: "h-2 w-2",
      lg: "h-2.5 w-2.5",
    },
    pulse: {
      true: "animate-pulse",
      false: "",
    },
  },
  defaultVariants: {
    status: "active",
    size: "md",
    pulse: false,
  },
});

export type StatusIndicatorStatus = "active" | "warning" | "error" | "inactive" | "pending";

interface StatusIndicatorProps extends VariantProps<typeof statusIndicatorVariants> {
  status: StatusIndicatorStatus;
  label?: string;
  pulse?: boolean;
  className?: string;
}

export function StatusIndicator({ status, label, size, pulse, className }: StatusIndicatorProps) {
  return (
    <span className={cn(statusIndicatorVariants({ size }), className)}>
      <span className={dotVariants({ status, size, pulse: pulse ?? status === "pending" })} />
      {label && <span className="text-muted-foreground">{label}</span>}
    </span>
  );
}
```

**Step 2: Export from atoms barrel**

In `apps/portal/src/components/atoms/index.ts`, add:

```ts
export { StatusIndicator, type StatusIndicatorStatus } from "./status-indicator";
```

**Step 3: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Commit**

```
feat(portal): add StatusIndicator shared component

Consistent dot + label component for status display across subscriptions,
orders, and support views. Supports 5 status variants with optional pulse.
```

---

## Task 6: New Shared Component — MetricCard

**Files:**

- Create: `apps/portal/src/components/molecules/MetricCard/MetricCard.tsx`
- Create: `apps/portal/src/components/molecules/MetricCard/index.ts`
- Modify: `apps/portal/src/components/molecules/index.ts`

**Step 1: Create the MetricCard component**

```tsx
import type { ReactNode } from "react";
import { cn } from "@/shared/utils";

type MetricTone = "primary" | "success" | "warning" | "danger" | "info" | "neutral";

const toneStyles: Record<MetricTone, { icon: string; accent: string }> = {
  primary: { icon: "text-primary bg-primary/10", accent: "text-primary" },
  success: { icon: "text-success bg-success/10", accent: "text-success" },
  warning: { icon: "text-warning bg-warning/10", accent: "text-warning" },
  danger: { icon: "text-danger bg-danger/10", accent: "text-danger" },
  info: { icon: "text-info bg-info/10", accent: "text-info" },
  neutral: { icon: "text-muted-foreground bg-muted", accent: "text-muted-foreground" },
};

export interface MetricCardProps {
  icon?: ReactNode;
  label: string;
  value: string | number;
  subtitle?: string;
  tone?: MetricTone;
  trend?: { value: string; positive?: boolean };
  className?: string;
}

export function MetricCard({
  icon,
  label,
  value,
  subtitle,
  tone = "primary",
  trend,
  className,
}: MetricCardProps) {
  const styles = toneStyles[tone];

  return (
    <div
      className={cn(
        "flex items-start gap-3.5 p-4 rounded-xl bg-card border border-border/60",
        "transition-all duration-200 hover:border-border hover:shadow-[var(--cp-shadow-1)]",
        className
      )}
    >
      {icon && (
        <div
          className={cn(
            "flex-shrink-0 h-10 w-10 rounded-lg flex items-center justify-center",
            styles.icon
          )}
        >
          {icon}
        </div>
      )}
      <div className="min-w-0 flex-1">
        <p className="text-xs font-medium text-muted-foreground">{label}</p>
        <div className="flex items-baseline gap-2 mt-0.5">
          <p className="text-2xl font-bold text-foreground tabular-nums font-display tracking-tight">
            {value}
          </p>
          {trend && (
            <span
              className={cn("text-xs font-medium", trend.positive ? "text-success" : "text-danger")}
            >
              {trend.value}
            </span>
          )}
        </div>
        {subtitle && <p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>}
      </div>
    </div>
  );
}

export function MetricCardSkeleton({ className }: { className?: string }) {
  return (
    <div
      className={cn(
        "flex items-start gap-3.5 p-4 rounded-xl bg-card border border-border/60",
        className
      )}
    >
      <div className="flex-shrink-0 h-10 w-10 rounded-lg cp-skeleton-shimmer" />
      <div className="min-w-0 flex-1 space-y-2">
        <div className="h-3 cp-skeleton-shimmer rounded w-16" />
        <div className="h-7 cp-skeleton-shimmer rounded w-12" />
      </div>
    </div>
  );
}
```

**Step 2: Create barrel file**

`apps/portal/src/components/molecules/MetricCard/index.ts`:

```ts
export { MetricCard, MetricCardSkeleton, type MetricCardProps } from "./MetricCard";
```

**Step 3: Export from molecules barrel**

In `apps/portal/src/components/molecules/index.ts`, add:

```ts
export { MetricCard, MetricCardSkeleton, type MetricCardProps } from "./MetricCard";
```

**Step 4: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 5: Commit**

```
feat(portal): add MetricCard shared component

Reusable metric display with icon, value, trend indicator, and tone-based
styling. Used across dashboard, subscriptions, and billing pages.
```

---

## Task 7: New Shared Component — ViewToggle

**Files:**

- Create: `apps/portal/src/components/atoms/view-toggle.tsx`
- Modify: `apps/portal/src/components/atoms/index.ts`

**Step 1: Create the ViewToggle component**

```tsx
"use client";

import { Squares2X2Icon, ListBulletIcon } from "@heroicons/react/24/outline";
import { cn } from "@/shared/utils";

export type ViewMode = "grid" | "list";

interface ViewToggleProps {
  value: ViewMode;
  onChange: (mode: ViewMode) => void;
  className?: string;
}

export function ViewToggle({ value, onChange, className }: ViewToggleProps) {
  return (
    <div
      className={cn(
        "inline-flex items-center rounded-lg border border-border/60 bg-muted/30 p-0.5",
        className
      )}
    >
      <button
        type="button"
        onClick={() => onChange("grid")}
        className={cn(
          "inline-flex items-center justify-center h-7 w-7 rounded-md transition-all duration-200",
          value === "grid"
            ? "bg-background text-foreground shadow-sm"
            : "text-muted-foreground hover:text-foreground"
        )}
        aria-label="Grid view"
        aria-pressed={value === "grid"}
      >
        <Squares2X2Icon className="h-3.5 w-3.5" />
      </button>
      <button
        type="button"
        onClick={() => onChange("list")}
        className={cn(
          "inline-flex items-center justify-center h-7 w-7 rounded-md transition-all duration-200",
          value === "list"
            ? "bg-background text-foreground shadow-sm"
            : "text-muted-foreground hover:text-foreground"
        )}
        aria-label="List view"
        aria-pressed={value === "list"}
      >
        <ListBulletIcon className="h-3.5 w-3.5" />
      </button>
    </div>
  );
}
```

**Step 2: Export from atoms barrel**

In `apps/portal/src/components/atoms/index.ts`, add:

```ts
export { ViewToggle, type ViewMode } from "./view-toggle";
```

**Step 3: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Commit**

```
feat(portal): add ViewToggle shared component

Grid/list toggle button pair for collection views. Used in subscriptions
and any future list pages.
```

---

## Task 8: Enhanced FilterBar Component

**Files:**

- Modify: `apps/portal/src/components/molecules/SearchFilterBar/SearchFilterBar.tsx`

**Step 1: Enhance the SearchFilterBar with active filter pills**

Read the current file first, then update it. Keep the existing props interface but add support for additional filter display and an optional ViewToggle integration:

```tsx
"use client";

import { MagnifyingGlassIcon, FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { cn } from "@/shared/utils";
import type { ReactNode } from "react";

export interface FilterOption {
  value: string;
  label: string;
}

export interface SearchFilterBarProps {
  searchValue: string;
  onSearchChange: (value: string) => void;
  searchPlaceholder?: string;
  filterValue?: string;
  onFilterChange?: (value: string) => void;
  filterOptions?: FilterOption[];
  filterLabel?: string;
  activeFilters?: { label: string; onRemove: () => void }[];
  children?: ReactNode;
}

export function SearchFilterBar({
  searchValue,
  onSearchChange,
  searchPlaceholder = "Search...",
  filterValue,
  onFilterChange,
  filterOptions,
  filterLabel = "Filter",
  activeFilters,
  children,
}: SearchFilterBarProps) {
  return (
    <div className="space-y-3">
      <div className="flex flex-col sm:flex-row gap-2.5">
        {/* Search */}
        <div className="relative flex-1 max-w-sm">
          <MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground/50" />
          <input
            type="text"
            value={searchValue}
            onChange={e => onSearchChange(e.target.value)}
            placeholder={searchPlaceholder}
            className="w-full h-9 pl-9 pr-3 rounded-lg border border-border/60 bg-background text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30 transition-all duration-200"
          />
        </div>

        <div className="flex items-center gap-2">
          {/* Filter select */}
          {filterOptions && onFilterChange && (
            <div className="relative">
              <select
                value={filterValue ?? "all"}
                onChange={e => onFilterChange(e.target.value)}
                className="h-9 pl-3 pr-8 rounded-lg border border-border/60 bg-background text-sm text-foreground appearance-none focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30 transition-all duration-200"
                aria-label={filterLabel}
              >
                {filterOptions.map(opt => (
                  <option key={opt.value} value={opt.value}>
                    {opt.label}
                  </option>
                ))}
              </select>
              <FunnelIcon className="absolute right-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50 pointer-events-none" />
            </div>
          )}

          {/* Custom actions (ViewToggle, etc.) */}
          {children}
        </div>
      </div>

      {/* Active filter pills */}
      {activeFilters && activeFilters.length > 0 && (
        <div className="flex flex-wrap gap-1.5">
          {activeFilters.map(filter => (
            <button
              key={filter.label}
              onClick={filter.onRemove}
              className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-primary/10 text-primary text-xs font-medium hover:bg-primary/20 transition-colors duration-150"
            >
              {filter.label}
              <XMarkIcon className="h-3 w-3" />
            </button>
          ))}
        </div>
      )}
    </div>
  );
}
```

**Step 2: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 3: Commit**

```
style(portal): enhance SearchFilterBar with active filter pills

- Add active filter pill display with remove buttons
- Accept children for custom actions (ViewToggle integration)
- Tighten visual styling and reduce border weight
```

---

## Task 9: Subscription Grid Card Component

**Files:**

- Create: `apps/portal/src/features/subscriptions/components/SubscriptionGridCard.tsx`
- Modify: `apps/portal/src/features/subscriptions/components/index.ts`

**Step 1: Create the grid card component**

```tsx
"use client";

import Link from "next/link";
import { CalendarDaysIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
import { StatusIndicator, type StatusIndicatorStatus } from "@/components/atoms";
import { cn } from "@/shared/utils";
import { useFormatCurrency } from "@/shared/hooks";
import type { Subscription, SubscriptionStatus } from "@customer-portal/domain/subscriptions";
import { SUBSCRIPTION_STATUS, SUBSCRIPTION_CYCLE } from "@customer-portal/domain/subscriptions";
import { getBillingCycleLabel } from "@/features/subscriptions/utils/status-presenters";

function mapSubscriptionStatus(status: SubscriptionStatus): StatusIndicatorStatus {
  switch (status) {
    case SUBSCRIPTION_STATUS.Active:
      return "active";
    case SUBSCRIPTION_STATUS.Pending:
      return "pending";
    case SUBSCRIPTION_STATUS.Suspended:
    case SUBSCRIPTION_STATUS.Cancelled:
      return "warning";
    case SUBSCRIPTION_STATUS.Terminated:
      return "error";
    default:
      return "inactive";
  }
}

interface SubscriptionGridCardProps {
  subscription: Subscription;
  className?: string;
}

export function SubscriptionGridCard({ subscription, className }: SubscriptionGridCardProps) {
  const formatCurrency = useFormatCurrency();
  const statusIndicator = mapSubscriptionStatus(subscription.status);
  const cycleLabel = getBillingCycleLabel(subscription.cycle);

  return (
    <Link
      href={`/account/subscriptions/${subscription.serviceId}`}
      className={cn(
        "group flex flex-col p-4 rounded-xl bg-card border border-border/60",
        "transition-all duration-200",
        "hover:border-border hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
        className
      )}
    >
      {/* Header: name + status */}
      <div className="flex items-start justify-between gap-2 mb-3">
        <div className="min-w-0 flex-1">
          <h3 className="text-sm font-semibold text-foreground truncate group-hover:text-primary transition-colors">
            {subscription.productName}
          </h3>
          <p className="text-xs text-muted-foreground mt-0.5 font-mono">
            #{subscription.serviceId}
          </p>
        </div>
        <StatusIndicator status={statusIndicator} label={subscription.status} size="sm" />
      </div>

      {/* Price */}
      <div className="mb-3">
        <span className="text-lg font-bold text-foreground tabular-nums font-display">
          {formatCurrency(subscription.amount, subscription.currency)}
        </span>
        {cycleLabel && <span className="text-xs text-muted-foreground ml-1">/{cycleLabel}</span>}
      </div>

      {/* Footer: next due + action */}
      <div className="flex items-center justify-between mt-auto pt-3 border-t border-border/40">
        {subscription.nextDue && (
          <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
            <CalendarDaysIcon className="h-3.5 w-3.5" />
            <span>
              {new Date(subscription.nextDue).toLocaleDateString("en-US", {
                month: "short",
                day: "numeric",
              })}
            </span>
          </div>
        )}
        <span className="inline-flex items-center gap-1 text-xs font-medium text-primary opacity-0 group-hover:opacity-100 transition-opacity duration-200">
          Manage
          <ArrowRightIcon className="h-3 w-3 group-hover:translate-x-0.5 transition-transform" />
        </span>
      </div>
    </Link>
  );
}

export function SubscriptionGridCardSkeleton() {
  return (
    <div className="flex flex-col p-4 rounded-xl bg-card border border-border/60">
      <div className="flex items-start justify-between gap-2 mb-3">
        <div className="space-y-1.5 flex-1">
          <div className="h-4 cp-skeleton-shimmer rounded w-3/4" />
          <div className="h-3 cp-skeleton-shimmer rounded w-16" />
        </div>
        <div className="h-4 cp-skeleton-shimmer rounded-full w-14" />
      </div>
      <div className="h-6 cp-skeleton-shimmer rounded w-20 mb-3" />
      <div className="pt-3 border-t border-border/40">
        <div className="h-3 cp-skeleton-shimmer rounded w-24" />
      </div>
    </div>
  );
}
```

**Step 2: Export from subscriptions components barrel**

In `apps/portal/src/features/subscriptions/components/index.ts`, add:

```ts
export { SubscriptionGridCard, SubscriptionGridCardSkeleton } from "./SubscriptionGridCard";
```

**Step 3: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Commit**

```
feat(portal): add SubscriptionGridCard component

Card-based subscription display for grid view with status indicator,
pricing, next due date, and hover-reveal manage action.
```

---

## Task 10: Subscriptions Page — Grid View & Enriched Stats

**Files:**

- Modify: `apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx`

**Step 1: Rewrite the SubscriptionsList view**

Replace the full contents of `SubscriptionsList.tsx`:

```tsx
"use client";

import { useState, useMemo } from "react";
import { Button } from "@/components/atoms/button";
import { ViewToggle, type ViewMode } from "@/components/atoms/view-toggle";
import { MetricCard, MetricCardSkeleton } from "@/components/molecules/MetricCard";
import { ErrorBoundary } from "@/components/molecules";
import { PageLayout } from "@/components/templates/PageLayout";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
import { SubscriptionTableSkeleton } from "@/components/atoms/loading-skeleton";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { SubscriptionTable } from "@/features/subscriptions/components/SubscriptionTable";
import {
  SubscriptionGridCard,
  SubscriptionGridCardSkeleton,
} from "@/features/subscriptions/components/SubscriptionGridCard";
import { Server, CheckCircle, XCircle, TrendingUp } from "lucide-react";
import { useSubscriptions, useSubscriptionStats } from "@/features/subscriptions/hooks";
import {
  SUBSCRIPTION_STATUS,
  type Subscription,
  type SubscriptionStatus,
} from "@customer-portal/domain/subscriptions";

const SUBSCRIPTION_STATUS_OPTIONS = Object.values(SUBSCRIPTION_STATUS) as SubscriptionStatus[];

function SubscriptionMetrics({
  stats,
}: {
  stats: { active: number; completed: number; cancelled: number };
}) {
  return (
    <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
      <MetricCard
        icon={<CheckCircle className="h-5 w-5" />}
        label="Active"
        value={stats.active}
        tone="success"
      />
      <MetricCard
        icon={<Server className="h-5 w-5" />}
        label="Total"
        value={stats.active + stats.completed + stats.cancelled}
        tone="primary"
      />
      <MetricCard
        icon={<TrendingUp className="h-5 w-5" />}
        label="Completed"
        value={stats.completed}
        tone="info"
      />
      <MetricCard
        icon={<XCircle className="h-5 w-5" />}
        label="Cancelled"
        value={stats.cancelled}
        tone="neutral"
      />
    </div>
  );
}

function SubscriptionMetricsSkeleton() {
  return (
    <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
      {Array.from({ length: 4 }).map((_, i) => (
        <MetricCardSkeleton key={i} />
      ))}
    </div>
  );
}

function SubscriptionGrid({
  subscriptions,
  loading,
}: {
  subscriptions: Subscription[];
  loading: boolean;
}) {
  if (subscriptions.length === 0 && !loading) {
    return (
      <div className="flex flex-col items-center justify-center py-16 text-center">
        <Server className="h-10 w-10 text-muted-foreground/30 mb-3" />
        <p className="text-sm font-medium text-foreground">No subscriptions found</p>
        <p className="text-xs text-muted-foreground mt-1">No active subscriptions at this time</p>
      </div>
    );
  }

  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 cp-stagger-children">
      {subscriptions.map(sub => (
        <SubscriptionGridCard key={sub.serviceId} subscription={sub} />
      ))}
      {loading &&
        Array.from({ length: 3 }).map((_, i) => <SubscriptionGridCardSkeleton key={`skel-${i}`} />)}
    </div>
  );
}

export function SubscriptionsListContainer() {
  const [searchTerm, setSearchTerm] = useState("");
  const [statusFilter, setStatusFilter] = useState<SubscriptionStatus | "all">("all");
  const [viewMode, setViewMode] = useState<ViewMode>("grid");

  const {
    data: subscriptionData,
    error,
    isFetching,
  } = useSubscriptions(statusFilter === "all" ? {} : { status: statusFilter });
  const { data: stats } = useSubscriptionStats();
  const showLoading = !subscriptionData && !error;

  const subscriptions = useMemo((): Subscription[] => {
    if (!subscriptionData) return [];
    if (Array.isArray(subscriptionData)) return subscriptionData;
    return subscriptionData.subscriptions;
  }, [subscriptionData]);

  const filteredSubscriptions = useMemo(() => {
    if (!searchTerm) return subscriptions;
    return subscriptions.filter(
      s =>
        s.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
        s.serviceId.toString().includes(searchTerm)
    );
  }, [subscriptions, searchTerm]);

  const statusFilterOptions = useMemo(
    () => [
      { value: "all" as const, label: "All Status" },
      ...SUBSCRIPTION_STATUS_OPTIONS.map(status => ({ value: status, label: status })),
    ],
    []
  );

  const activeFilters = useMemo(() => {
    const filters: { label: string; onRemove: () => void }[] = [];
    if (statusFilter !== "all") {
      filters.push({ label: `Status: ${statusFilter}`, onRemove: () => setStatusFilter("all") });
    }
    if (searchTerm) {
      filters.push({ label: `Search: ${searchTerm}`, onRemove: () => setSearchTerm("") });
    }
    return filters;
  }, [statusFilter, searchTerm]);

  if (showLoading || error) {
    return (
      <PageLayout
        icon={<Server />}
        title="Subscriptions"
        description="Manage your active subscriptions"
      >
        <AsyncBlock isLoading={false} error={error}>
          <div className="space-y-6">
            <SubscriptionMetricsSkeleton />
            <SubscriptionTableSkeleton rows={6} />
          </div>
        </AsyncBlock>
      </PageLayout>
    );
  }

  return (
    <PageLayout
      icon={<Server />}
      title="Subscriptions"
      description="Manage your active subscriptions"
      actions={
        <Button as="a" href="/account/services" size="sm">
          Browse Services
        </Button>
      }
    >
      <ErrorBoundary>
        {stats && <SubscriptionMetrics stats={stats} />}

        <div className="bg-card rounded-xl border border-border/60 shadow-[var(--cp-shadow-1)] overflow-hidden">
          <div className="px-4 sm:px-5 py-3.5 border-b border-border/40">
            <SearchFilterBar
              searchValue={searchTerm}
              onSearchChange={setSearchTerm}
              searchPlaceholder="Search subscriptions..."
              filterValue={statusFilter}
              onFilterChange={value => setStatusFilter(value as SubscriptionStatus | "all")}
              filterOptions={statusFilterOptions}
              filterLabel="Filter by status"
              activeFilters={activeFilters.length > 0 ? activeFilters : undefined}
            >
              <ViewToggle value={viewMode} onChange={setViewMode} />
            </SearchFilterBar>
          </div>

          <div className="p-4 sm:p-5">
            {viewMode === "grid" ? (
              <SubscriptionGrid
                subscriptions={filteredSubscriptions}
                loading={isFetching && !!subscriptionData}
              />
            ) : (
              <SubscriptionTable
                subscriptions={filteredSubscriptions}
                loading={isFetching && !!subscriptionData}
                className="border-0 rounded-none shadow-none"
              />
            )}
          </div>
        </div>
      </ErrorBoundary>
    </PageLayout>
  );
}

export default SubscriptionsListContainer;
```

**Step 2: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 3: Commit**

```
feat(portal): overhaul subscriptions page with grid view and metrics

- Replace stat cards with 4-column MetricCard grid (active, total, completed, cancelled)
- Add grid/list view toggle with default grid view
- Grid displays subscription cards with status, price, and next due date
- Add active filter pills for search and status filters
- Keep existing table view as list mode option
```

---

## Task 11: Dashboard — Enhanced Control Center Layout

**Files:**

- Modify: `apps/portal/src/features/dashboard/views/DashboardView.tsx`
- Modify: `apps/portal/src/features/dashboard/components/QuickStats.tsx`

**Step 1: Update DashboardGreeting for a cleaner look**

In `DashboardView.tsx`, update the `DashboardGreeting` component:

```tsx
function DashboardGreeting({
  displayName,
  taskCount,
  hasUrgentTask,
}: {
  displayName: string;
  taskCount: number;
  hasUrgentTask: boolean;
}) {
  return (
    <div className="mb-8">
      <h2 className="text-2xl sm:text-3xl font-bold text-foreground font-display tracking-tight animate-in fade-in slide-in-from-bottom-2 duration-500">
        Welcome back, {displayName}
      </h2>
      {taskCount > 0 ? (
        <div
          className="flex items-center gap-2 mt-2 animate-in fade-in slide-in-from-bottom-4 duration-500"
          style={{ animationDelay: "50ms" }}
        >
          <span
            className={cn(
              "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
              hasUrgentTask ? "bg-danger/10 text-danger" : "bg-warning/10 text-warning"
            )}
          >
            {hasUrgentTask && <ExclamationTriangleIcon className="h-3.5 w-3.5" />}
            {taskCount === 1 ? "1 task needs attention" : `${taskCount} tasks need attention`}
          </span>
        </div>
      ) : (
        <p
          className="text-sm text-muted-foreground mt-1.5 animate-in fade-in slide-in-from-bottom-4 duration-500"
          style={{ animationDelay: "50ms" }}
        >
          Everything is up to date
        </p>
      )}
    </div>
  );
}
```

Key changes: Combined greeting into single line, removed "Welcome back" as separate element, tightened spacing.

**Step 2: Update QuickStats to use MetricCard style**

In `QuickStats.tsx`, update `StatItem` to have tighter, more refined styling:

```tsx
function StatItem({ icon: Icon, label, value, href, tone = "primary", emptyText }: StatItemProps) {
  const styles = toneStyles[tone];

  return (
    <Link
      href={href}
      className={cn(
        "group flex items-center gap-3.5 p-3.5 rounded-xl",
        "bg-card border border-border/60",
        "transition-all duration-200",
        "hover:border-border hover:shadow-[var(--cp-shadow-1)] hover:-translate-y-0.5",
        styles.hoverBorder
      )}
    >
      <div
        className={cn(
          "flex-shrink-0 h-10 w-10 rounded-lg flex items-center justify-center",
          "transition-all duration-200",
          styles.iconBg
        )}
      >
        <Icon className={cn("h-4.5 w-4.5", styles.iconColor)} />
      </div>
      <div className="min-w-0 flex-1">
        <p className="text-xs font-medium text-muted-foreground">{label}</p>
        {value > 0 ? (
          <p className="text-xl font-bold text-foreground tabular-nums mt-0.5 font-display">
            {value}
          </p>
        ) : (
          <p className="text-xs text-muted-foreground mt-1">{emptyText || "None"}</p>
        )}
      </div>
      <ArrowRightIcon className="h-3.5 w-3.5 text-muted-foreground/30 group-hover:text-foreground/60 group-hover:translate-x-0.5 transition-all flex-shrink-0" />
    </Link>
  );
}
```

**Step 3: Verify**

Run: `pnpm type-check`
Expected: PASS

Run: `pnpm lint`
Expected: PASS

**Step 4: Commit**

```
style(portal): refine dashboard greeting and quick stats

- Combine greeting into single "Welcome back, Name" heading
- Tighten QuickStats card styling with reduced padding and font sizes
- Use font-display for numeric values
- Lighter hover states and borders
```

---

## Task 12: Button Variant — Add `subtle` Variant

**Files:**

- Modify: `apps/portal/src/components/atoms/button.tsx`

**Step 1: Add the `subtle` variant to buttonVariants**

In the `variant` object inside `buttonVariants` (around line 12), add after `ghost`:

```ts
subtle:
  "bg-muted/50 text-foreground hover:bg-muted border border-transparent hover:border-border/40",
```

**Step 2: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 3: Commit**

```
feat(portal): add subtle button variant

Faint background tint button for secondary actions inside cards. Sits
between ghost (transparent) and outline (bordered).
```

---

## Task 13: AppShell Layout — Reduce Sidebar Width & Background Texture

**Files:**

- Modify: `apps/portal/src/components/organisms/AppShell/AppShell.tsx`
- Modify: `apps/portal/src/styles/tokens.css`

**Step 1: Reduce sidebar width from 240px to 220px**

In `AppShell.tsx`, update the desktop sidebar container (around line 202):

```tsx
<div className="flex flex-col w-[220px] border-r border-sidebar-border/40 bg-sidebar">
```

In `tokens.css`, update the sidebar width variable (line 57):

```css
--cp-sidebar-width: 13.75rem; /* 220px */
```

**Step 2: Add a subtle background pattern to the main content area**

In `AppShell.tsx`, update the main content wrapper (around line 213):

```tsx
<div className="flex flex-col w-0 flex-1 overflow-hidden bg-background">
```

No additional texture needed - the warmer neutrals from Task 2 already provide enough visual interest. Keep it clean.

**Step 3: Verify**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Commit**

```
style(portal): reduce sidebar width to 220px

Tighter sidebar gives more room to content. Update both the CSS
variable and the inline width.
```

---

## Task 14: Motion Refinements — Card Entrance & Hover Polish

**Files:**

- Modify: `apps/portal/src/styles/utilities.css`

**Step 1: Add card grid stagger animation**

Add after the existing stagger children rules (around line 243):

```css
/* Card grid stagger - faster delay for dense grids */
.cp-stagger-grid > * {
  opacity: 0;
  animation: cp-fade-up var(--cp-duration-normal) var(--cp-ease-out) forwards;
}

.cp-stagger-grid > *:nth-child(1) {
  animation-delay: 0ms;
}
.cp-stagger-grid > *:nth-child(2) {
  animation-delay: 30ms;
}
.cp-stagger-grid > *:nth-child(3) {
  animation-delay: 60ms;
}
.cp-stagger-grid > *:nth-child(4) {
  animation-delay: 90ms;
}
.cp-stagger-grid > *:nth-child(5) {
  animation-delay: 120ms;
}
.cp-stagger-grid > *:nth-child(6) {
  animation-delay: 150ms;
}
.cp-stagger-grid > *:nth-child(n + 7) {
  animation-delay: 180ms;
}
```

**Step 2: Add to reduced motion rules**

In the `@media (prefers-reduced-motion: reduce)` block, add `.cp-stagger-grid > *` to the selector list.

**Step 3: Verify**

Run: `pnpm lint`
Expected: PASS

**Step 4: Commit**

```
style(portal): add grid stagger animation for subscription cards

Faster 30ms stagger delay for dense grid layouts. Respects
prefers-reduced-motion.
```

---

## Task 15: Final Verification & Type Check

**Step 1: Run full type check**

Run: `pnpm type-check`
Expected: PASS with no errors

**Step 2: Run linting**

Run: `pnpm lint`
Expected: PASS with no errors

**Step 3: Run tests**

Run: `pnpm test`
Expected: All existing tests pass (changes are purely visual)

**Step 4: Final commit if any fixes needed**

```
chore(portal): fix any type or lint issues from UI overhaul
```

---

## Dependency Graph

```
Task 1 (fonts) ──────────────┐
Task 2 (colors) ─────────────┤
                              ├──> Task 3 (sidebar) ──────┐
                              ├──> Task 4 (header)         │
                              ├──> Task 5 (StatusIndicator) ├──> Task 9 (grid card) ──> Task 10 (subscriptions page)
                              ├──> Task 6 (MetricCard) ────┘                           │
                              ├──> Task 7 (ViewToggle) ─────────────────────────────────┘
                              ├──> Task 8 (FilterBar) ──────────────────────────────────┘
                              ├──> Task 12 (button variant)
                              └──> Task 14 (motion)

Task 11 (dashboard) depends on Task 6 (MetricCard)
Task 13 (AppShell width) is independent
Task 15 (verification) depends on all tasks
```

**Independent task groups that can be parallelized:**

- Group A: Tasks 1, 2 (design tokens — must go first)
- Group B: Tasks 3, 4, 13 (shell components — after Group A)
- Group C: Tasks 5, 6, 7, 8, 12, 14 (shared components — after Group A)
- Group D: Tasks 9, 10 (subscriptions — after Group C)
- Group E: Task 11 (dashboard — after Task 6)
- Group F: Task 15 (verification — after all)
