diff --git a/apps/portal/src/components/atoms/animated-container.tsx b/apps/portal/src/components/atoms/animated-container.tsx
new file mode 100644
index 00000000..8b16dd47
--- /dev/null
+++ b/apps/portal/src/components/atoms/animated-container.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { cn } from "@/shared/utils";
+
+interface AnimatedContainerProps {
+ children: React.ReactNode;
+ className?: string;
+ /** Animation type */
+ animation?: "fade-up" | "fade-scale" | "slide-left" | "none";
+ /** Whether to stagger children animations */
+ stagger?: boolean;
+ /** Delay before animation starts in ms */
+ delay?: number;
+}
+
+/**
+ * Reusable animation wrapper component
+ * Provides consistent entrance animations for page content
+ */
+export function AnimatedContainer({
+ children,
+ className,
+ animation = "fade-up",
+ stagger = false,
+ delay = 0,
+}: AnimatedContainerProps) {
+ const animationClass = {
+ "fade-up": "cp-animate-in",
+ "fade-scale": "cp-animate-scale-in",
+ "slide-left": "cp-animate-slide-left",
+ none: "",
+ }[animation];
+
+ return (
+
0 ? { animationDelay: `${delay}ms` } : undefined}
+ >
+ {children}
+
+ );
+}
diff --git a/apps/portal/src/components/atoms/loading-overlay.tsx b/apps/portal/src/components/atoms/loading-overlay.tsx
new file mode 100644
index 00000000..53d64e7c
--- /dev/null
+++ b/apps/portal/src/components/atoms/loading-overlay.tsx
@@ -0,0 +1,41 @@
+import { Spinner } from "./spinner";
+
+interface LoadingOverlayProps {
+ /** Whether the overlay is visible */
+ isVisible: boolean;
+ /** Main loading message */
+ title: string;
+ /** Optional subtitle/description */
+ subtitle?: string;
+ /** Spinner size */
+ spinnerSize?: "xs" | "sm" | "md" | "lg" | "xl";
+ /** Custom spinner color */
+ spinnerClassName?: string;
+ /** Custom overlay background */
+ overlayClassName?: string;
+}
+
+export function LoadingOverlay({
+ isVisible,
+ title,
+ subtitle,
+ spinnerSize = "xl",
+ spinnerClassName = "text-primary",
+ overlayClassName = "bg-background/80 backdrop-blur-sm",
+}: LoadingOverlayProps) {
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+
+ );
+}
diff --git a/apps/portal/src/components/atoms/skeleton.tsx b/apps/portal/src/components/atoms/skeleton.tsx
new file mode 100644
index 00000000..eff285b3
--- /dev/null
+++ b/apps/portal/src/components/atoms/skeleton.tsx
@@ -0,0 +1,20 @@
+import { cn } from "@/shared/utils";
+
+interface SkeletonProps {
+ className?: string;
+ animate?: boolean;
+}
+
+/**
+ * Base skeleton atom for loading states.
+ * A simple shimmer box primitive that can be composed into loading patterns.
+ *
+ * For composed loading skeletons, use:
+ * - LoadingCard, LoadingTable, LoadingStats from molecules/LoadingSkeletons
+ * - Feature-specific skeletons from features/[feature]/components/skeletons
+ */
+export function Skeleton({ className, animate = true }: SkeletonProps) {
+ return (
+
+ );
+}
diff --git a/apps/portal/src/components/atoms/spinner.tsx b/apps/portal/src/components/atoms/spinner.tsx
new file mode 100644
index 00000000..e2e1afda
--- /dev/null
+++ b/apps/portal/src/components/atoms/spinner.tsx
@@ -0,0 +1,31 @@
+import { cn } from "@/shared/utils";
+
+interface SpinnerProps {
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
+ className?: string;
+}
+
+const sizeClasses = {
+ xs: "h-3 w-3",
+ sm: "h-4 w-4",
+ md: "h-6 w-6",
+ lg: "h-8 w-8",
+ xl: "h-10 w-10",
+};
+
+export function Spinner({ size = "sm", className }: SpinnerProps) {
+ return (
+
+ );
+}
diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/index.ts b/apps/portal/src/components/molecules/LoadingSkeletons/index.ts
new file mode 100644
index 00000000..4ce945d8
--- /dev/null
+++ b/apps/portal/src/components/molecules/LoadingSkeletons/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Loading Skeleton Molecules
+ * Generic, reusable loading skeleton components for common UI patterns.
+ */
+
+export { LoadingCard } from "./loading-card";
+export { LoadingTable } from "./loading-table";
+export { LoadingStats } from "./loading-stats";
diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx
new file mode 100644
index 00000000..c7018902
--- /dev/null
+++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-card.tsx
@@ -0,0 +1,36 @@
+import { cn } from "@/shared/utils";
+import { Skeleton } from "@/components/atoms";
+
+interface LoadingCardProps {
+ className?: string;
+}
+
+/**
+ * Generic loading skeleton for card-like content.
+ * Shows a header with avatar and two lines, plus body text lines.
+ */
+export function LoadingCard({ className }: LoadingCardProps) {
+ return (
+
+ );
+}
diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx
new file mode 100644
index 00000000..32de0cdb
--- /dev/null
+++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-stats.tsx
@@ -0,0 +1,31 @@
+import { Skeleton } from "@/components/atoms";
+
+interface LoadingStatsProps {
+ /** Number of stat cards to display */
+ count?: number;
+}
+
+/**
+ * Generic loading skeleton for stats/metrics cards.
+ * Shows a grid of stat cards with icon and two value lines.
+ */
+export function LoadingStats({ count = 4 }: LoadingStatsProps) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx b/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx
new file mode 100644
index 00000000..33ab9ca2
--- /dev/null
+++ b/apps/portal/src/components/molecules/LoadingSkeletons/loading-table.tsx
@@ -0,0 +1,40 @@
+import { Skeleton } from "@/components/atoms";
+
+interface LoadingTableProps {
+ /** Number of rows to display */
+ rows?: number;
+ /** Number of columns to display */
+ columns?: number;
+}
+
+/**
+ * Generic loading skeleton for table-like content.
+ * Shows a header row and configurable number of body rows.
+ */
+export function LoadingTable({ rows = 5, columns = 4 }: LoadingTableProps) {
+ return (
+
+ {/* Header */}
+
+
+ {Array.from({ length: columns }).map((_, i) => (
+
+ ))}
+
+
+
+ {/* Rows */}
+
+ {Array.from({ length: rows }).map((_, rowIndex) => (
+
+
+ {Array.from({ length: columns }).map((_, colIndex) => (
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/portal/src/components/molecules/StatusBadge/index.ts b/apps/portal/src/components/molecules/StatusBadge/index.ts
new file mode 100644
index 00000000..48a80b9e
--- /dev/null
+++ b/apps/portal/src/components/molecules/StatusBadge/index.ts
@@ -0,0 +1,9 @@
+/**
+ * StatusBadge Molecule
+ *
+ * A configurable status display component that wraps StatusPill
+ * with domain-specific configuration maps.
+ */
+
+export { StatusBadge } from "./status-badge";
+export type { StatusBadgeProps, StatusConfig, StatusConfigMap } from "./types";
diff --git a/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx b/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx
new file mode 100644
index 00000000..ecf6c4fa
--- /dev/null
+++ b/apps/portal/src/components/molecules/StatusBadge/status-badge.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { forwardRef } from "react";
+import { StatusPill } from "@/components/atoms/status-pill";
+import type { StatusBadgeProps, StatusConfig } from "./types";
+
+const DEFAULT_CONFIG: StatusConfig = {
+ variant: "neutral",
+};
+
+/**
+ * StatusBadge - A configurable status display component.
+ *
+ * Wraps the StatusPill atom with a configuration-driven approach,
+ * allowing domain-specific status mappings to be passed in.
+ *
+ * @example
+ * ```tsx
+ * // Simple usage with inline config
+ *
+ *
+ * // Or create a domain-specific wrapper
+ * const ORDER_STATUS_CONFIG = {
+ * pending: { variant: "warning", icon: , label: "Pending" },
+ * completed: { variant: "success", icon: , label: "Completed" },
+ * };
+ *
+ * function OrderStatusBadge({ status }) {
+ * return ;
+ * }
+ * ```
+ */
+export const StatusBadge = forwardRef(
+ ({ status, configMap, showIcon = true, defaultConfig = DEFAULT_CONFIG, ...props }, ref) => {
+ // Normalize status to lowercase for config lookup
+ const normalizedStatus = status.toLowerCase();
+
+ // Look up config, fall back to default
+ const config = configMap?.[normalizedStatus as keyof typeof configMap] ?? defaultConfig;
+
+ // Determine the label (config label, or original status)
+ const label = config.label ?? status;
+
+ return (
+
+ );
+ }
+);
+
+StatusBadge.displayName = "StatusBadge";
diff --git a/apps/portal/src/components/molecules/StatusBadge/types.ts b/apps/portal/src/components/molecules/StatusBadge/types.ts
new file mode 100644
index 00000000..b5efbb78
--- /dev/null
+++ b/apps/portal/src/components/molecules/StatusBadge/types.ts
@@ -0,0 +1,40 @@
+import type { ReactNode } from "react";
+import type { StatusPillProps } from "@/components/atoms/status-pill";
+
+/**
+ * Status configuration for a single status value.
+ * Maps a status string to its visual representation.
+ */
+export interface StatusConfig {
+ /** The visual variant/color for this status */
+ variant: StatusPillProps["variant"];
+ /** Optional icon to display */
+ icon?: ReactNode;
+ /** Display label (defaults to the status key if not provided) */
+ label?: string;
+}
+
+/**
+ * A map of status values to their configurations.
+ * Keys should be lowercase for case-insensitive matching.
+ */
+export type StatusConfigMap = Partial<
+ Record, StatusConfig>
+>;
+
+/**
+ * Props for the StatusBadge molecule.
+ */
+export interface StatusBadgeProps extends Omit<
+ StatusPillProps,
+ "variant" | "icon" | "label"
+> {
+ /** The status value to display */
+ status: T;
+ /** Configuration map for status values */
+ configMap?: StatusConfigMap;
+ /** Whether to show the icon (default: true) */
+ showIcon?: boolean;
+ /** Default config for unknown statuses */
+ defaultConfig?: StatusConfig;
+}
diff --git a/apps/portal/src/features/billing/components/skeletons/index.ts b/apps/portal/src/features/billing/components/skeletons/index.ts
new file mode 100644
index 00000000..4e14fad2
--- /dev/null
+++ b/apps/portal/src/features/billing/components/skeletons/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Billing Loading Skeletons
+ * Feature-specific skeletons for consistent loading states.
+ */
+
+export { InvoiceListSkeleton } from "./invoice-list-skeleton";
diff --git a/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx b/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx
new file mode 100644
index 00000000..3f7e5ff4
--- /dev/null
+++ b/apps/portal/src/features/billing/components/skeletons/invoice-list-skeleton.tsx
@@ -0,0 +1,31 @@
+import { Skeleton } from "@/components/atoms";
+
+interface InvoiceListSkeletonProps {
+ /** Number of rows to show */
+ rows?: number;
+}
+
+/**
+ * List skeleton for invoices/billing.
+ * Shows a heading and list items with left content and right price.
+ */
+export function InvoiceListSkeleton({ rows = 5 }: InvoiceListSkeletonProps) {
+ return (
+
+
+
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/portal/src/features/services/views/AccountServicesOverview.tsx b/apps/portal/src/features/services/views/AccountServicesOverview.tsx
new file mode 100644
index 00000000..5b8cbab3
--- /dev/null
+++ b/apps/portal/src/features/services/views/AccountServicesOverview.tsx
@@ -0,0 +1,28 @@
+import { ServicesGrid } from "@/features/services/components/common/ServicesGrid";
+
+/**
+ * AccountServicesOverview - Authenticated user's services landing page.
+ *
+ * Shows available services for the logged-in user with a header
+ * and services grid linking to account-specific service pages.
+ */
+export function AccountServicesOverview() {
+ return (
+
+
+ {/* Header */}
+
+
+ Our Services
+
+
+ From high-speed internet to onsite support, we provide comprehensive solutions for your
+ home and business.
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/features/services/views/PublicServicesOverview.tsx b/apps/portal/src/features/services/views/PublicServicesOverview.tsx
new file mode 100644
index 00000000..71401ec1
--- /dev/null
+++ b/apps/portal/src/features/services/views/PublicServicesOverview.tsx
@@ -0,0 +1,157 @@
+import Link from "next/link";
+import {
+ Wifi,
+ Smartphone,
+ ShieldCheck,
+ ArrowRight,
+ Phone,
+ CheckCircle2,
+ Globe,
+ Headphones,
+ Building2,
+ Wrench,
+ Tv,
+} from "lucide-react";
+import { ServiceCard } from "@/components/molecules/ServiceCard";
+
+/**
+ * PublicServicesOverview - Public-facing services landing page.
+ *
+ * Shows all available services with hero section, value propositions,
+ * service cards grid, and contact CTA.
+ */
+export function PublicServicesOverview() {
+ return (
+
+ {/* Hero */}
+
+
+
+
+ Full English Support
+
+
+
+
+ Our Services
+
+
+
+ Connectivity and support solutions for Japan's international community.
+
+
+
+ {/* Value Props - Compact */}
+
+
+
+ One provider, all services
+
+
+
+ English support
+
+
+
+ No hidden fees
+
+
+
+ {/* All Services - Clean Grid with staggered animations */}
+
+ }
+ title="Internet"
+ description="NTT Optical Fiber for homes and apartments. Speeds up to 10Gbps with professional installation."
+ price="¥3,200/mo"
+ accentColor="blue"
+ />
+
+ }
+ title="SIM & eSIM"
+ description="Data, voice & SMS on NTT Docomo network. Physical SIM or instant eSIM activation."
+ price="¥1,100/mo"
+ badge="1st month free"
+ accentColor="green"
+ />
+
+ }
+ title="VPN Router"
+ description="Access US & UK streaming content with a pre-configured router. Simple plug-and-play."
+ price="¥2,500/mo"
+ accentColor="purple"
+ />
+
+ }
+ title="Business"
+ description="Enterprise solutions for offices and commercial spaces. Dedicated support and SLAs."
+ accentColor="orange"
+ />
+
+ }
+ title="Onsite Support"
+ description="Professional technicians visit your location for setup, troubleshooting, and maintenance."
+ accentColor="cyan"
+ />
+
+ }
+ title="TV"
+ description="Streaming TV packages with international channels. Watch content from home countries."
+ accentColor="pink"
+ />
+
+
+ {/* CTA */}
+
+ Need help choosing?
+
+ Our bilingual team can help you find the right solution.
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/features/subscriptions/components/skeletons/index.ts b/apps/portal/src/features/subscriptions/components/skeletons/index.ts
new file mode 100644
index 00000000..327b73db
--- /dev/null
+++ b/apps/portal/src/features/subscriptions/components/skeletons/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Subscription Loading Skeletons
+ * Feature-specific skeletons for consistent loading states.
+ */
+
+export { SubscriptionStatsCardsSkeleton } from "./subscription-stats-cards-skeleton";
+export { SubscriptionTableSkeleton } from "./subscription-table-skeleton";
+export { SubscriptionDetailStatsSkeleton } from "./subscription-detail-stats-skeleton";
diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx
new file mode 100644
index 00000000..aa17bfaa
--- /dev/null
+++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-detail-stats-skeleton.tsx
@@ -0,0 +1,30 @@
+import { Skeleton } from "@/components/atoms";
+
+/**
+ * Stats skeleton for the subscription detail page.
+ * Shows 4 columns: Status, Amount, Next Due, Registration.
+ */
+export function SubscriptionDetailStatsSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx
new file mode 100644
index 00000000..113c2c47
--- /dev/null
+++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-stats-cards-skeleton.tsx
@@ -0,0 +1,23 @@
+import { Skeleton } from "@/components/atoms";
+
+/**
+ * Stats cards skeleton for the subscriptions list page.
+ * Shows 3 stat cards with icon and value placeholders.
+ */
+export function SubscriptionStatsCardsSkeleton() {
+ return (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx b/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx
new file mode 100644
index 00000000..d29df542
--- /dev/null
+++ b/apps/portal/src/features/subscriptions/components/skeletons/subscription-table-skeleton.tsx
@@ -0,0 +1,50 @@
+import { Skeleton } from "@/components/atoms";
+
+interface SubscriptionTableSkeletonProps {
+ /** Number of rows to show */
+ rows?: number;
+}
+
+/**
+ * Table skeleton for the subscriptions list.
+ * Shows 3 columns: Service, Amount, Next Due.
+ */
+export function SubscriptionTableSkeleton({ rows = 6 }: SubscriptionTableSkeletonProps) {
+ return (
+
+
+
+
+
+ {/* Header skeleton */}
+
+ {/* Row skeletons */}
+
+ {Array.from({ length: rows }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}