diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
index a21c01df..94e2c5b6 100644
--- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
+++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
@@ -205,7 +205,7 @@ function SubscriptionDetailContent({
tone="primary"
actions={
View Invoices
diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx
index e7f285cf..e1458796 100644
--- a/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx
+++ b/apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useMemo } from "react";
+import { motion } from "framer-motion";
import { Button } from "@/components/atoms/button";
import { ViewToggle, type ViewMode } from "@/components/atoms/view-toggle";
import { MetricCard, MetricCardSkeleton } from "@/components/molecules/MetricCard";
@@ -22,6 +23,16 @@ import {
type SubscriptionStatus,
} from "@customer-portal/domain/subscriptions";
+const gridContainerVariants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.03 } },
+};
+
+const gridItemVariants = {
+ hidden: { opacity: 0, y: 12 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.2, ease: "easeOut" as const } },
+};
+
const SUBSCRIPTION_STATUS_OPTIONS = Object.values(SUBSCRIPTION_STATUS) as SubscriptionStatus[];
function SubscriptionMetrics({
@@ -87,13 +98,20 @@ function SubscriptionGrid({
}
return (
-
+
{subscriptions.map(sub => (
-
+
+
+
))}
{loading &&
Array.from({ length: 3 }).map((_, i) => )}
-
+
);
}
diff --git a/apps/portal/src/shared/hooks/index.ts b/apps/portal/src/shared/hooks/index.ts
index d52b2d66..8de54e79 100644
--- a/apps/portal/src/shared/hooks/index.ts
+++ b/apps/portal/src/shared/hooks/index.ts
@@ -5,4 +5,3 @@ export { useZodForm } from "./useZodForm";
export { useCurrency } from "./useCurrency";
export { useFormatCurrency, type FormatCurrencyOptions } from "./useFormatCurrency";
export { useCountUp } from "./useCountUp";
-export { useAfterPaint } from "./useAfterPaint";
diff --git a/apps/portal/src/shared/hooks/useAfterPaint.ts b/apps/portal/src/shared/hooks/useAfterPaint.ts
deleted file mode 100644
index bee1a651..00000000
--- a/apps/portal/src/shared/hooks/useAfterPaint.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-
-/**
- * Schedules a callback after the browser has painted, using a double-rAF.
- * The first frame lets the browser commit the current DOM state,
- * the second frame runs the callback after that paint is on screen.
- *
- * Useful for re-enabling CSS transitions after an instant DOM snap.
- */
-export function useAfterPaint(callback: () => void, enabled: boolean) {
- useEffect(() => {
- if (!enabled) return;
- let id1 = 0;
- let id2 = 0;
- id1 = requestAnimationFrame(() => {
- id2 = requestAnimationFrame(callback);
- });
- return () => {
- cancelAnimationFrame(id1);
- cancelAnimationFrame(id2);
- };
- }, [enabled, callback]);
-}
diff --git a/apps/portal/src/shared/hooks/useCountUp.ts b/apps/portal/src/shared/hooks/useCountUp.ts
index 78db1f57..ead496dd 100644
--- a/apps/portal/src/shared/hooks/useCountUp.ts
+++ b/apps/portal/src/shared/hooks/useCountUp.ts
@@ -1,27 +1,16 @@
"use client";
-import { useState, useEffect, useRef } from "react";
-
-const reducedMotionQuery =
- typeof window === "undefined" ? undefined : window.matchMedia("(prefers-reduced-motion: reduce)");
+import { useState, useEffect } from "react";
+import { animate, useReducedMotion } from "framer-motion";
interface UseCountUpOptions {
- /** Starting value (default: 0) */
start?: number;
- /** Target value to count to */
end: number;
- /** Animation duration in ms (default: 300) */
duration?: number;
- /** Delay before starting animation in ms (default: 0) */
delay?: number;
- /** Whether animation is enabled (default: true) */
enabled?: boolean;
}
-/**
- * Animated counter hook for stats and numbers
- * Uses requestAnimationFrame for smooth 60fps animation
- */
export function useCountUp({
start = 0,
end,
@@ -30,8 +19,7 @@ export function useCountUp({
enabled = true,
}: UseCountUpOptions): number {
const [count, setCount] = useState(start);
- const frameRef = useRef(undefined);
- const startTimeRef = useRef(undefined);
+ const prefersReducedMotion = useReducedMotion();
useEffect(() => {
if (!enabled) {
@@ -39,42 +27,28 @@ export function useCountUp({
return;
}
- // Respect prefers-reduced-motion — show final value immediately
- if (reducedMotionQuery?.matches) {
+ if (prefersReducedMotion) {
setCount(end);
return;
}
- startTimeRef.current = undefined;
+ let controls: ReturnType | undefined;
const timeout = setTimeout(() => {
- const animate = (timestamp: number) => {
- if (!startTimeRef.current) {
- startTimeRef.current = timestamp;
- }
-
- const progress = Math.min((timestamp - startTimeRef.current) / duration, 1);
- // Ease-out cubic for smooth deceleration
- const eased = 1 - Math.pow(1 - progress, 3);
- const next = Math.round(start + (end - start) * eased);
-
- setCount(prev => (prev === next ? prev : next));
-
- if (progress < 1) {
- frameRef.current = requestAnimationFrame(animate);
- }
- };
-
- frameRef.current = requestAnimationFrame(animate);
+ controls = animate(start, end, {
+ duration: duration / 1000,
+ ease: [0, 0, 0.2, 1],
+ onUpdate: value => {
+ setCount(Math.round(value));
+ },
+ });
}, delay);
return () => {
clearTimeout(timeout);
- if (frameRef.current) {
- cancelAnimationFrame(frameRef.current);
- }
+ controls?.stop();
};
- }, [start, end, duration, delay, enabled]);
+ }, [start, end, duration, delay, enabled, prefersReducedMotion]);
return count;
}
diff --git a/apps/portal/src/styles/utilities.css b/apps/portal/src/styles/utilities.css
index 698f1796..92f2cd6b 100644
--- a/apps/portal/src/styles/utilities.css
+++ b/apps/portal/src/styles/utilities.css
@@ -7,39 +7,6 @@
/* ===== KEYFRAMES ===== */
-@keyframes cp-fade-up {
- from {
- opacity: 0;
- transform: translateY(var(--cp-translate-lg));
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes cp-fade-scale {
- from {
- opacity: 0;
- transform: scale(0.95);
- }
- to {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-@keyframes cp-slide-in-left {
- from {
- opacity: 0;
- transform: translateX(calc(var(--cp-translate-xl) * -1));
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
@keyframes cp-shimmer {
0% {
transform: translateX(-100%);
@@ -49,28 +16,6 @@
}
}
-@keyframes cp-toast-enter {
- from {
- opacity: 0;
- transform: translateX(100%) scale(0.9);
- }
- to {
- opacity: 1;
- transform: translateX(0) scale(1);
- }
-}
-
-@keyframes cp-toast-exit {
- from {
- opacity: 1;
- transform: translateX(0) scale(1);
- }
- to {
- opacity: 0;
- transform: translateX(100%) scale(0.9);
- }
-}
-
@keyframes cp-shake {
0%,
100% {
@@ -86,69 +31,6 @@
}
}
-@keyframes cp-activity-enter {
- from {
- opacity: 0;
- transform: translateX(-8px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-@keyframes cp-slide-fade-left {
- from {
- opacity: 0;
- transform: translateX(24px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-@keyframes cp-slide-fade-right {
- from {
- opacity: 0;
- transform: translateX(-24px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-@keyframes cp-float {
- 0%,
- 100% {
- transform: translateY(0px) rotate(0deg);
- }
- 50% {
- transform: translateY(-20px) rotate(2deg);
- }
-}
-
-@keyframes cp-float-slow {
- 0%,
- 100% {
- transform: translateY(0px) rotate(0deg);
- }
- 50% {
- transform: translateY(-12px) rotate(-1deg);
- }
-}
-
-@keyframes cp-pulse-glow {
- 0%,
- 100% {
- box-shadow: 0 0 0 0 var(--primary);
- }
- 50% {
- box-shadow: 0 0 20px 4px color-mix(in oklch, var(--primary) 40%, transparent);
- }
-}
-
/* Legacy shimmer animation for compatibility */
@keyframes cp-skeleton-shimmer {
0% {
@@ -208,100 +90,6 @@
}
@layer utilities {
- /* ===== PAGE ENTRANCE ANIMATIONS ===== */
- .cp-animate-in {
- animation: cp-fade-up var(--cp-duration-slow) var(--cp-ease-out) forwards;
- }
-
- .cp-animate-scale-in {
- animation: cp-fade-scale var(--cp-duration-normal) var(--cp-ease-out) forwards;
- }
-
- .cp-animate-slide-left {
- animation: cp-slide-in-left var(--cp-duration-slow) var(--cp-ease-out) forwards;
- }
-
- /* Staggered children animation */
- .cp-stagger-children > * {
- opacity: 0;
- animation: cp-fade-up var(--cp-duration-slow) var(--cp-ease-out) forwards;
- }
-
- .cp-stagger-children > *:nth-child(1) {
- animation-delay: var(--cp-stagger-1);
- }
- .cp-stagger-children > *:nth-child(2) {
- animation-delay: var(--cp-stagger-2);
- }
- .cp-stagger-children > *:nth-child(3) {
- animation-delay: var(--cp-stagger-3);
- }
- .cp-stagger-children > *:nth-child(4) {
- animation-delay: var(--cp-stagger-4);
- }
- .cp-stagger-children > *:nth-child(5) {
- animation-delay: var(--cp-stagger-5);
- }
- .cp-stagger-children > *:nth-child(n + 6) {
- animation-delay: calc(var(--cp-stagger-5) + 50ms);
- }
-
- /* 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;
- }
-
- /* ===== TAB SLIDE TRANSITIONS ===== */
- .cp-slide-fade-left {
- animation: cp-slide-fade-left 300ms var(--cp-ease-out) forwards;
- }
-
- .cp-slide-fade-right {
- animation: cp-slide-fade-right 300ms var(--cp-ease-out) forwards;
- }
-
- /* ===== CARD HOVER LIFT ===== */
- .cp-card-hover-lift {
- transition:
- transform var(--cp-duration-normal) var(--cp-ease-out),
- box-shadow var(--cp-duration-normal) var(--cp-ease-out);
- }
-
- .cp-card-hover-lift:hover {
- transform: translateY(-2px);
- box-shadow:
- 0 10px 40px -10px rgb(0 0 0 / 0.15),
- 0 4px 6px -2px rgb(0 0 0 / 0.05);
- }
-
- .cp-card-hover-lift:active {
- transform: translateY(0);
- transition-duration: var(--cp-duration-fast);
- }
-
/* ===== SKELETON SHIMMER ===== */
.cp-skeleton-shimmer {
position: relative;
@@ -339,50 +127,6 @@
animation: cp-shake var(--cp-duration-slow) var(--cp-ease-out);
}
- /* ===== TOAST ANIMATIONS ===== */
- .cp-toast-enter {
- animation: cp-toast-enter var(--cp-duration-slow) var(--cp-ease-spring) forwards;
- }
-
- .cp-toast-exit {
- animation: cp-toast-exit var(--cp-duration-normal) var(--cp-ease-in) forwards;
- }
-
- /* ===== ACTIVITY FEED ===== */
- .cp-activity-item {
- opacity: 0;
- animation: cp-activity-enter var(--cp-duration-normal) var(--cp-ease-out) forwards;
- }
-
- .cp-activity-item:nth-child(1) {
- animation-delay: 0ms;
- }
- .cp-activity-item:nth-child(2) {
- animation-delay: 50ms;
- }
- .cp-activity-item:nth-child(3) {
- animation-delay: 100ms;
- }
- .cp-activity-item:nth-child(4) {
- animation-delay: 150ms;
- }
- .cp-activity-item:nth-child(5) {
- animation-delay: 200ms;
- }
-
- /* ===== FLOAT ANIMATIONS ===== */
- .cp-float {
- animation: cp-float 6s ease-in-out infinite;
- }
-
- .cp-float-slow {
- animation: cp-float-slow 8s ease-in-out infinite;
- }
-
- .cp-float-delayed {
- animation: cp-float 6s ease-in-out infinite 2s;
- }
-
/* ===== GLASS MORPHISM ===== */
.cp-glass {
background: var(--glass-bg);
@@ -477,10 +221,6 @@
box-shadow: var(--shadow-primary-lg);
}
- .cp-glow-pulse {
- animation: cp-pulse-glow 2s ease-in-out infinite;
- }
-
/* ===== PREMIUM CARD VARIANTS ===== */
.cp-card-glass {
background: var(--glass-bg);
@@ -700,21 +440,9 @@
/* ===== ACCESSIBILITY: REDUCED MOTION ===== */
@media (prefers-reduced-motion: reduce) {
- .cp-animate-in,
- .cp-animate-scale-in,
- .cp-animate-slide-left,
- .cp-stagger-children > *,
- .cp-stagger-grid > *,
- .cp-card-hover-lift,
- .cp-slide-fade-left,
- .cp-slide-fade-right,
- .cp-toast-enter,
- .cp-toast-exit,
- .cp-activity-item,
- .cp-float,
- .cp-float-slow,
- .cp-float-delayed,
- .cp-glow-pulse {
+ .cp-skeleton-shimmer::after,
+ .cp-skeleton::after,
+ .cp-input-error-shake {
animation: none !important;
transition: none !important;
opacity: 1 !important;
diff --git a/docs/plans/2026-03-06-sidebar-consolidation.md b/docs/plans/2026-03-06-sidebar-consolidation.md
new file mode 100644
index 00000000..3ea374d3
--- /dev/null
+++ b/docs/plans/2026-03-06-sidebar-consolidation.md
@@ -0,0 +1,458 @@
+# Sidebar Navigation Consolidation
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Simplify sidebar by removing expandable sub-menus — make Support and Billing flat links, creating a combined Billing page.
+
+**Architecture:** Remove children from navigation items, create a new combined Billing page view that composes existing PaymentMethods and InvoicesList components, update route structure to add `/account/billing` page.
+
+**Tech Stack:** Next.js 15, React 19, Tailwind CSS, existing billing/support feature components
+
+---
+
+### Task 1: Make Support a flat sidebar link
+
+**Files:**
+
+- Modify: `apps/portal/src/components/organisms/AppShell/navigation.ts:46-49`
+
+**Step 1: Update navigation config**
+
+Change the Support entry from expandable (with children) to a flat link:
+
+```ts
+// Before (lines 46-49):
+{
+ name: "Support",
+ icon: ChatBubbleLeftRightIcon,
+ children: [
+ { name: "Cases", href: "/account/support" },
+ { name: "New Case", href: "/account/support/new" },
+ ],
+},
+
+// After:
+{ name: "Support", href: "/account/support", icon: ChatBubbleLeftRightIcon },
+```
+
+**Step 2: Remove auto-expand logic for Support in AppShell**
+
+File: `apps/portal/src/components/organisms/AppShell/AppShell.tsx:112`
+
+Remove the line:
+
+```ts
+if (pathname.startsWith("/account/support")) next.add("Support");
+```
+
+**Step 3: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS — no type errors
+
+**Step 4: Commit**
+
+```bash
+git add apps/portal/src/components/organisms/AppShell/navigation.ts apps/portal/src/components/organisms/AppShell/AppShell.tsx
+git commit -m "refactor: make Support a flat sidebar link"
+```
+
+---
+
+### Task 2: Create combined Billing page view
+
+**Files:**
+
+- Create: `apps/portal/src/features/billing/views/BillingOverview.tsx`
+
+**Step 1: Create the combined billing view**
+
+This view composes existing `PaymentMethodsContainer` content and `InvoicesList` into one page. We reuse the existing components directly — payment methods section on top, invoices below.
+
+```tsx
+"use client";
+
+import { useState } from "react";
+import { CreditCardIcon } from "@heroicons/react/24/outline";
+import { PageLayout } from "@/components/templates/PageLayout";
+import { ErrorBoundary } from "@/components/molecules";
+import { useSession } from "@/features/auth/hooks";
+import { useAuthStore } from "@/features/auth/stores/auth.store";
+import { isApiError } from "@/core/api";
+import { openSsoLink } from "@/features/billing/utils/sso";
+import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh";
+import {
+ PaymentMethodCard,
+ usePaymentMethods,
+ useCreatePaymentMethodsSsoLink,
+} from "@/features/billing";
+import type { PaymentMethodList } from "@customer-portal/domain/payments";
+import { InlineToast } from "@/components/atoms/inline-toast";
+import { Button } from "@/components/atoms/button";
+import { Skeleton } from "@/components/atoms/loading-skeleton";
+import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
+import { logger } from "@/core/logger";
+
+function PaymentMethodsSkeleton() {
+ return (
+
+
+
+
+
+
+ {Array.from({ length: 2 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
+
+function PaymentMethodsSection({
+ paymentMethodsData,
+ onManage,
+ isPending,
+}: {
+ paymentMethodsData: PaymentMethodList;
+ onManage: () => void;
+ isPending: boolean;
+}) {
+ const hasMethods = paymentMethodsData.paymentMethods.length > 0;
+
+ return (
+
+
+
+
+
Payment Methods
+
+ {hasMethods
+ ? `${paymentMethodsData.paymentMethods.length} payment method${paymentMethodsData.paymentMethods.length === 1 ? "" : "s"} on file`
+ : "No payment methods on file"}
+
+
+
+ {isPending ? "Opening..." : "Manage Cards"}
+
+
+
+ {hasMethods && (
+
+
+ {paymentMethodsData.paymentMethods.map(paymentMethod => (
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+export function BillingOverview() {
+ const [error, setError] = useState(null);
+ const { isAuthenticated } = useSession();
+ const paymentMethodsQuery = usePaymentMethods();
+ const {
+ data: paymentMethodsData,
+ isLoading: isLoadingPaymentMethods,
+ isFetching: isFetchingPaymentMethods,
+ error: paymentMethodsError,
+ } = paymentMethodsQuery;
+ const createPaymentMethodsSsoLink = useCreatePaymentMethodsSsoLink();
+ const { hasCheckedAuth } = useAuthStore();
+
+ const paymentRefresh = usePaymentRefresh({
+ refetch: async () => {
+ const result = await paymentMethodsQuery.refetch();
+ return { data: result.data };
+ },
+ hasMethods: data => Boolean(data && (data.totalCount > 0 || data.paymentMethods.length > 0)),
+ attachFocusListeners: true,
+ });
+
+ const openPaymentMethods = async () => {
+ if (!isAuthenticated) {
+ setError("Please log in to access payment methods.");
+ return;
+ }
+ setError(null);
+ try {
+ const ssoLink = await createPaymentMethodsSsoLink.mutateAsync();
+ openSsoLink(ssoLink.url, { newTab: true });
+ } catch (err: unknown) {
+ logger.error("Failed to open payment methods", err);
+ if (
+ isApiError(err) &&
+ "response" in err &&
+ typeof err.response === "object" &&
+ err.response !== null &&
+ "status" in err.response &&
+ err.response.status === 401
+ ) {
+ setError("Authentication failed. Please log in again.");
+ } else {
+ setError("Unable to access payment methods. Please try again later.");
+ }
+ }
+ };
+
+ const isPaymentLoading = !hasCheckedAuth || isLoadingPaymentMethods || isFetchingPaymentMethods;
+ const combinedError = error
+ ? new Error(error)
+ : paymentMethodsError instanceof Error
+ ? paymentMethodsError
+ : paymentMethodsError
+ ? new Error(String(paymentMethodsError))
+ : null;
+
+ return (
+ } title="Billing" error={combinedError}>
+
+
+
+
+ {/* Payment Methods Section */}
+ {isPaymentLoading &&
}
+ {!isPaymentLoading && paymentMethodsData && (
+
void openPaymentMethods()}
+ isPending={createPaymentMethodsSsoLink.isPending}
+ />
+ )}
+
+ {/* Invoices Section */}
+
+
Invoices
+
+
+
+
+
+ );
+}
+
+export default BillingOverview;
+```
+
+**Step 2: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add apps/portal/src/features/billing/views/BillingOverview.tsx
+git commit -m "feat: create combined BillingOverview view"
+```
+
+---
+
+### Task 3: Add /account/billing route and make sidebar flat
+
+**Files:**
+
+- Create: `apps/portal/src/app/account/billing/page.tsx`
+- Modify: `apps/portal/src/components/organisms/AppShell/navigation.ts:29-36`
+- Modify: `apps/portal/src/components/organisms/AppShell/AppShell.tsx:111`
+
+**Step 1: Create the billing page**
+
+```tsx
+import { BillingOverview } from "@/features/billing/views/BillingOverview";
+
+export default function AccountBillingPage() {
+ return ;
+}
+```
+
+**Step 2: Update navigation config**
+
+Change Billing from expandable to flat:
+
+```ts
+// Before (lines 29-36):
+{
+ name: "Billing",
+ icon: CreditCardIcon,
+ children: [
+ { name: "Invoices", href: "/account/billing/invoices" },
+ { name: "Payment Methods", href: "/account/billing/payments" },
+ ],
+},
+
+// After:
+{ name: "Billing", href: "/account/billing", icon: CreditCardIcon },
+```
+
+**Step 3: Remove auto-expand logic for Billing in AppShell**
+
+File: `apps/portal/src/components/organisms/AppShell/AppShell.tsx`
+
+Remove the line:
+
+```ts
+if (pathname.startsWith("/account/billing")) next.add("Billing");
+```
+
+**Step 4: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add apps/portal/src/app/account/billing/page.tsx apps/portal/src/components/organisms/AppShell/navigation.ts apps/portal/src/components/organisms/AppShell/AppShell.tsx
+git commit -m "refactor: make Billing a flat sidebar link with combined page"
+```
+
+---
+
+### Task 4: Update Sidebar active-state matching for flat Billing and Support
+
+**Files:**
+
+- Modify: `apps/portal/src/components/organisms/AppShell/Sidebar.tsx:327`
+
+The current `SimpleNavItem` uses exact match (`pathname === item.href`) which won't highlight Billing when on `/account/billing/invoices/123`. Change to `startsWith` for path-based matching:
+
+```ts
+// Before (line 327):
+const isActive = item.href ? pathname === item.href : false;
+
+// After:
+const isActive = item.href
+ ? item.href === "/account"
+ ? pathname === item.href
+ : pathname.startsWith(item.href)
+ : false;
+```
+
+This ensures:
+
+- Dashboard (`/account`) still uses exact match (doesn't highlight for every `/account/*` page)
+- Billing (`/account/billing`) highlights on `/account/billing`, `/account/billing/invoices/123`, etc.
+- Support (`/account/support`) highlights on `/account/support`, `/account/support/new`, `/account/support/123`, etc.
+
+**Step 1: Update Sidebar active matching**
+
+Apply the change above.
+
+**Step 2: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add apps/portal/src/components/organisms/AppShell/Sidebar.tsx
+git commit -m "fix: use startsWith for sidebar active state on nested routes"
+```
+
+---
+
+### Task 5: Update backLink references in InvoiceDetail
+
+**Files:**
+
+- Modify: `apps/portal/src/features/billing/views/InvoiceDetail.tsx:84,99`
+
+Update the "Back to Invoices" links to point to the combined billing page:
+
+```ts
+// Before:
+backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}
+
+// After:
+backLink={{ label: "Back to Billing", href: "/account/billing" }}
+```
+
+Apply this on both lines 84 and 99.
+
+**Step 1: Apply changes**
+
+**Step 2: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add apps/portal/src/features/billing/views/InvoiceDetail.tsx
+git commit -m "fix: update InvoiceDetail backLink to point to combined billing page"
+```
+
+---
+
+### Task 6: Update remaining billing route references
+
+**Files:**
+
+- Modify: `apps/portal/src/features/dashboard/utils/dashboard.utils.ts:43` — change `/account/billing/invoices/${activity.relatedId}` to keep as-is (invoice detail pages still live at `/account/billing/invoices/[id]`)
+- Modify: `apps/portal/src/features/dashboard/components/TaskList.tsx:66` — change `href="/account/billing/invoices"` to `href="/account/billing"`
+- Modify: `apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx:208` — change `href="/account/billing/invoices"` to `href="/account/billing"`
+- Modify: `apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx:160,187` — change `href="/account/billing/invoices"` to `href="/account/billing"`
+
+Note: Keep `dashboard.utils.ts:43` unchanged — it links to a specific invoice detail page which still exists at `/account/billing/invoices/[id]`.
+Note: Keep `InvoiceTable.tsx:276` unchanged — it navigates to individual invoice detail pages.
+
+**Step 1: Apply changes to the 3 files listed above**
+
+**Step 2: Verify**
+
+Run: `pnpm type-check`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add apps/portal/src/features/dashboard/components/TaskList.tsx apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx
+git commit -m "fix: update billing route references to use combined billing page"
+```
+
+---
+
+### Task 7: Clean up unused imports in navigation.ts
+
+**Files:**
+
+- Modify: `apps/portal/src/components/organisms/AppShell/navigation.ts`
+
+After removing children from both Billing and Support, the `NavigationChild` type and children-related interfaces may still be needed by other code (the Sidebar still supports expandable items generically). Check if `NavigationChild` is still used — if Subscriptions or any other item still has children, keep it. If no items have children anymore, remove unused types.
+
+**Step 1: Check if any navigation item still uses children**
+
+After our changes, review `baseNavigation` — none will have children. But `NavigationChild` and `children` field on `NavigationItem` are still referenced by `Sidebar.tsx` (the `ExpandableNavItem` component). These can stay for now since they're part of the generic nav system — removing the component is a larger cleanup.
+
+**Step 2: Verify full build**
+
+Run: `pnpm type-check && pnpm lint`
+Expected: PASS
+
+**Step 3: Final commit if any cleanup was needed**
+
+```bash
+git add -A
+git commit -m "chore: sidebar consolidation cleanup"
+```