diff --git a/apps/web/src/app/hotel/[id]/escrow/[escrowId]/page.tsx b/apps/web/src/app/hotel/[id]/escrow/[escrowId]/page.tsx
new file mode 100644
index 0000000..52dac08
--- /dev/null
+++ b/apps/web/src/app/hotel/[id]/escrow/[escrowId]/page.tsx
@@ -0,0 +1,297 @@
+// TODO: replace stub views with real components once merged in frontend-SafeTrust
+// Sources:
+// frontend-SafeTrust/src/components/escrow/views/EscrowPaidView.tsx
+// frontend-SafeTrust/src/components/escrow/views/EscrowBlockedView.tsx
+// frontend-SafeTrust/src/components/escrow/views/EscrowReleasedView.tsx
+// frontend-SafeTrust/src/components/escrow/RealTimeEscrowStatus.tsx
+//
+// Status-to-view mapping (when wired):
+// funded -> EscrowPaidView (Payment batch)
+// active -> EscrowBlockedView (Deposit blocked)
+// completed -> EscrowReleasedView (Deposit released)
+// default -> redirect to /hotel/[id]/escrow/create
+//
+// Real-time: RealTimeEscrowStatus (Hasura subscription) drives automatic transitions
+
+import { InvoiceHeader } from '@/components/escrow/InvoiceHeader';
+import { ProcessStepper } from '@/components/escrow/ProcessStepper';
+import type { CSSProperties } from 'react';
+
+type StubStatus = 'paid' | 'blocked' | 'released';
+
+type ViewConfig = {
+ label: StubStatus;
+ step: 2 | 3 | 4;
+ title: string;
+};
+
+const styles = {
+ page: {
+ maxWidth: '72rem',
+ margin: '0 auto',
+ padding: '2rem 1.5rem 3rem',
+ color: '#111827',
+ } satisfies CSSProperties,
+ grid: {
+ display: 'grid',
+ gap: '1.5rem',
+ marginTop: '1.5rem',
+ alignItems: 'start',
+ } satisfies CSSProperties,
+ panel: {
+ border: '1px solid #fed7aa',
+ borderRadius: '1rem',
+ backgroundColor: '#ffffff',
+ padding: '1.5rem',
+ } satisfies CSSProperties,
+ splitGrid: {
+ display: 'grid',
+ gap: '1rem',
+ gridTemplateColumns: 'repeat(auto-fit, minmax(13rem, 1fr))',
+ } satisfies CSSProperties,
+ table: {
+ width: '100%',
+ borderCollapse: 'collapse',
+ fontSize: '0.95rem',
+ } satisfies CSSProperties,
+ input: {
+ width: '100%',
+ border: '1px solid #d1d5db',
+ borderRadius: '0.75rem',
+ padding: '0.75rem',
+ font: 'inherit',
+ resize: 'vertical',
+ minHeight: '6rem',
+ } satisfies CSSProperties,
+ buttonRow: {
+ display: 'flex',
+ gap: '0.75rem',
+ marginTop: '0.75rem',
+ flexWrap: 'wrap',
+ } satisfies CSSProperties,
+ secondaryButton: {
+ border: '1px solid #d1d5db',
+ backgroundColor: '#ffffff',
+ color: '#111827',
+ borderRadius: '0.75rem',
+ padding: '0.6rem 1rem',
+ fontWeight: 600,
+ } satisfies CSSProperties,
+ primaryButton: {
+ border: '1px solid #f97316',
+ backgroundColor: '#f97316',
+ color: '#ffffff',
+ borderRadius: '0.75rem',
+ padding: '0.6rem 1rem',
+ fontWeight: 700,
+ } satisfies CSSProperties,
+} as const;
+
+function getStubView(status: string | undefined): ViewConfig {
+ switch (status) {
+ case 'blocked':
+ return { label: 'blocked', step: 3, title: 'Payment batch - Escrow Status' };
+ case 'released':
+ return { label: 'released', step: 4, title: 'Deposit / Escrow Released' };
+ case 'paid':
+ default:
+ return { label: 'paid', step: 2, title: 'Payment batch January 2025' };
+ }
+}
+
+function InfoPair({ label, value }: { label: string; value: string }) {
+ return (
+
+ );
+}
+
+function PaidStubView() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ | PRODUCT |
+ PRICE / MONTH |
+ DEPOSIT |
+
+
+
+
+ | La sabana apartment |
+
+ $4,000
+ |
+
+ $4,000
+ |
+
+
+
+
+
+
+ Total: $8,000
+
+
+ );
+}
+
+function BlockedStubView() {
+ return (
+
+
+
+
+
+
+
+
+
Tenant Information
+
+
+
+
+
+
+
+
Owner Information
+
+
+
+
+
+
+
+
+ );
+}
+
+function ReleasedStubView() {
+ return (
+
+
+
+
Escrow Justification
+
+
+
+
+
+
+
+
Beneficiary Information
+
+
+
+
+
+
+
+
+
+
Claims
+
+
+
+
+
+
+
+
+ );
+}
+
+export default function EscrowDetailPage({
+ params,
+ searchParams,
+}: {
+ params: { id: string; escrowId: string };
+ searchParams: { status?: string };
+}) {
+ const view = getStubView(searchParams?.status);
+
+ return (
+
+
+
+
+
+
+ Hotel {params.id}
+
+
{view.title}
+
+ {/* TODO: swap placeholder sections for real escrow views once frontend-SafeTrust is merged */}
+ {view.label === 'paid' &&
}
+ {view.label === 'blocked' &&
}
+ {view.label === 'released' &&
}
+
+
+
+
+
+
+ Dev: append ?status=paid, ?status=blocked, or{' '}
+ ?status=released to preview each view state.
+
+
+ );
+}
diff --git a/apps/web/src/app/hotel/[id]/page.tsx b/apps/web/src/app/hotel/[id]/page.tsx
new file mode 100644
index 0000000..22c8471
--- /dev/null
+++ b/apps/web/src/app/hotel/[id]/page.tsx
@@ -0,0 +1,189 @@
+// TODO: replace with real detail components once merged in frontend-SafeTrust
+// Sources:
+// frontend-SafeTrust/src/components/hotel/ApartmentDetail.tsx
+// frontend-SafeTrust/src/components/hotel/ImageGallery.tsx
+// frontend-SafeTrust/src/components/hotel/SuggestionsList.tsx
+// frontend-SafeTrust/src/components/hotel/AmenityIcons.tsx
+//
+// Data source (when wired):
+// Apollo query: GET_APARTMENT_BY_ID -> public.apartments (Hasura)
+
+import Link from 'next/link';
+import type { CSSProperties } from 'react';
+
+const STUB_APARTMENT = {
+ name: 'La sabana sur',
+ address: '329 Calle santos, paseo colón, San José',
+ price: 4058,
+ bedrooms: 2,
+ bathrooms: 1,
+ petFriendly: true,
+ owner: {
+ name: 'Alberto Casas',
+ email: 'albertoCasas100@gmail.com',
+ phone: '+506 64852179',
+ },
+ description:
+ 'Beautiful apartment in the heart of San José with modern amenities and stunning views.',
+};
+
+const styles = {
+ page: {
+ display: 'flex',
+ minHeight: '100vh',
+ backgroundColor: '#fff7ed',
+ color: '#111827',
+ } satisfies CSSProperties,
+ aside: {
+ width: '18rem',
+ borderRight: '1px solid #fed7aa',
+ padding: '1rem',
+ flexShrink: 0,
+ display: 'none',
+ } satisfies CSSProperties,
+ suggestionCard: {
+ display: 'flex',
+ gap: '0.5rem',
+ padding: '0.5rem',
+ marginBottom: '0.5rem',
+ border: '1px solid #fdba74',
+ borderRadius: '0.75rem',
+ backgroundColor: '#ffffff',
+ fontSize: '0.75rem',
+ } satisfies CSSProperties,
+ imagePlaceholder: {
+ backgroundColor: '#fed7aa',
+ borderRadius: '1rem',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: '#9a3412',
+ } satisfies CSSProperties,
+ main: {
+ flex: 1,
+ maxWidth: '64rem',
+ padding: '1.5rem',
+ } satisfies CSSProperties,
+ gallery: {
+ display: 'grid',
+ gridTemplateColumns: '2fr 1fr',
+ gap: '0.5rem',
+ marginBottom: '1.5rem',
+ } satisfies CSSProperties,
+ thumbs: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '0.5rem',
+ } satisfies CSSProperties,
+ header: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ gap: '1rem',
+ marginBottom: '1rem',
+ } satisfies CSSProperties,
+ button: {
+ display: 'inline-block',
+ backgroundColor: '#f97316',
+ color: '#ffffff',
+ fontWeight: 700,
+ padding: '0.75rem 1.5rem',
+ borderRadius: '0.75rem',
+ } satisfies CSSProperties,
+ metaRow: {
+ display: 'flex',
+ gap: '1rem',
+ marginBottom: '1rem',
+ flexWrap: 'wrap',
+ } satisfies CSSProperties,
+} as const;
+
+export default function ApartmentDetailPage({
+ params,
+}: {
+ params: { id: string };
+}) {
+ return (
+
+ {/* TODO: replace with
*/}
+
+
+
+ {/* TODO: replace with */}
+
+
+ Main image
+
+
+ {[1, 2, 3].map((i) => (
+
+ Thumb {i}
+
+ ))}
+
+
+
+
+
+
{STUB_APARTMENT.name}
+
+ ${STUB_APARTMENT.price.toLocaleString()}.00 Per month
+
+
+
+
+ BOOK
+
+
+
+ {/* TODO: replace with */}
+
+ 📍 {STUB_APARTMENT.address}
+
+
+ 🛏 {STUB_APARTMENT.bedrooms} bd
+ 🐾 {STUB_APARTMENT.petFriendly ? 'pet friendly' : 'no pets'}
+ 🚿 {STUB_APARTMENT.bathrooms} ba
+
+
+
+
+ Owner: {STUB_APARTMENT.owner.name}
+
+
+
+
+
Apartment details
+
{STUB_APARTMENT.description}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/escrow/InvoiceHeader.tsx b/apps/web/src/components/escrow/InvoiceHeader.tsx
new file mode 100644
index 0000000..a10f87d
--- /dev/null
+++ b/apps/web/src/components/escrow/InvoiceHeader.tsx
@@ -0,0 +1,93 @@
+import type { CSSProperties } from 'react';
+
+type InvoiceStatus = 'paid' | 'blocked' | 'released';
+
+const STATUS_STYLES: Record = {
+ paid: {
+ label: 'Paid',
+ style: {
+ backgroundColor: '#ffedd5',
+ color: '#9a3412',
+ border: '1px solid #fdba74',
+ },
+ },
+ blocked: {
+ label: 'Deposit blocked',
+ style: {
+ backgroundColor: '#fef3c7',
+ color: '#92400e',
+ border: '1px solid #fcd34d',
+ },
+ },
+ released: {
+ label: 'Deposit released',
+ style: {
+ backgroundColor: '#dcfce7',
+ color: '#166534',
+ border: '1px solid #86efac',
+ },
+ },
+};
+
+export function InvoiceHeader({
+ invoiceNumber,
+ paidAt,
+ status,
+}: {
+ invoiceNumber: string;
+ paidAt: string;
+ status: InvoiceStatus;
+}) {
+ const badge = STATUS_STYLES[status];
+
+ return (
+
+
+
+ Invoice number
+
+
{invoiceNumber}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/escrow/ProcessStepper.tsx b/apps/web/src/components/escrow/ProcessStepper.tsx
new file mode 100644
index 0000000..1e22c69
--- /dev/null
+++ b/apps/web/src/components/escrow/ProcessStepper.tsx
@@ -0,0 +1,68 @@
+import type { CSSProperties } from 'react';
+
+const STEPS = [
+ { step: 1, title: 'Create escrow' },
+ { step: 2, title: 'Payment batch' },
+ { step: 3, title: 'Deposit blocked' },
+ { step: 4, title: 'Deposit released' },
+] as const;
+
+export function ProcessStepper({ currentStep }: { currentStep: 1 | 2 | 3 | 4 }) {
+ return (
+
+
Process
+
+ {STEPS.map(({ step, title }) => {
+ const isActive = step === currentStep;
+ const isComplete = step < currentStep;
+ const markerStyle: CSSProperties = isActive
+ ? { backgroundColor: '#f97316', color: '#ffffff', border: '1px solid #f97316' }
+ : isComplete
+ ? { backgroundColor: '#ffedd5', color: '#9a3412', border: '1px solid #fdba74' }
+ : { backgroundColor: '#ffffff', color: '#9ca3af', border: '1px solid #d1d5db' };
+
+ return (
+
+
+ {step}
+
+
+
+ {title}
+
+
+ {isActive ? 'Current step' : isComplete ? 'Completed' : 'Pending'}
+
+
+
+ );
+ })}
+
+
+ );
+}