diff --git a/src/components/molecules/ComparisonColumnCard/ComparisonColumnCard.tsx b/src/components/molecules/ComparisonColumnCard/ComparisonColumnCard.tsx
index 22ec8d7..5b2597c 100644
--- a/src/components/molecules/ComparisonColumnCard/ComparisonColumnCard.tsx
+++ b/src/components/molecules/ComparisonColumnCard/ComparisonColumnCard.tsx
@@ -67,17 +67,17 @@ export const ComparisonColumnCard = React.forwardRef
+
) : (
-
+
)
}
sx={{
position: 'absolute',
- top: -12,
+ top: -13,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1,
@@ -109,20 +109,23 @@ export const ComparisonColumnCard = React.forwardRef
- {/* Provider name with optional verified icon (truncated with tooltip) */}
+ {/* Provider name — always reserves space for 2 lines (via minHeight),
+ content bottom-aligned so single-line names sit flush with the
+ next item below rather than floating high in the slot. */}
{pkg.isRecommended && (
@@ -131,6 +134,7 @@ export const ComparisonColumnCard = React.forwardRef
@@ -139,15 +143,17 @@ export const ComparisonColumnCard = React.forwardRef
@@ -179,18 +185,29 @@ export const ComparisonColumnCard = React.forwardRef
)}
-
+
{pkg.name}
-
- Total package price
-
-
- {formatPrice(pkg.price)}
-
+ {/* Price subgroup — tighter internal spacing than the outer gap
+ so the label sits close to the amount it describes. */}
+
+
+ Total package price
+
+
+ {formatPrice(pkg.price)}
+
+
{/* Spacer pushes CTA to bottom across all cards */}
@@ -200,28 +217,33 @@ export const ComparisonColumnCard = React.forwardRef onArrange(pkg.id)}
- sx={{ mt: 1.5, px: 4 }}
+ sx={{ px: 4 }}
>
{pkg.provider.verified ? 'Make Arrangement' : 'Make Enquiry'}
- {!pkg.isRecommended && onRemove ? (
- onRemove(pkg.id)}
- sx={{ mt: 0.5 }}
- >
- Remove
-
- ) : (
- /* Invisible spacer keeps CTA aligned with cards that show Remove */
-
- Remove
-
- )}
+ {/* Always render the same Link element; hide when no Remove action
+ applies (recommended or no handler). Keeps the footer row
+ identical across all cards so CTAs align. */}
+ {(() => {
+ const canRemove = !pkg.isRecommended && !!onRemove;
+ return (
+ onRemove!(pkg.id) : undefined}
+ tabIndex={canRemove ? 0 : -1}
+ aria-hidden={!canRemove}
+ sx={{
+ ...(!canRemove && { visibility: 'hidden', pointerEvents: 'none' }),
+ }}
+ >
+ Remove
+
+ );
+ })()}
diff --git a/src/components/organisms/ComparisonTable/ComparisonTable.tsx b/src/components/organisms/ComparisonTable/ComparisonTable.tsx
index 930fe37..658022d 100644
--- a/src/components/organisms/ComparisonTable/ComparisonTable.tsx
+++ b/src/components/organisms/ComparisonTable/ComparisonTable.tsx
@@ -63,7 +63,55 @@ function formatPrice(amount: number): string {
return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`;
}
-function CellValue({ value }: { value: ComparisonCellValue }) {
+/**
+ * Inline icon + label wrapper with optically aligned centres.
+ *
+ * body2's line-height adds vertical padding above/below the glyphs. Flex
+ * centring then aligns geometric centres, which puts the icon slightly
+ * above the text's visual centre. Setting `lineHeight: 1` on the row
+ * collapses the text line-box to the font size so geometric and visual
+ * centres match.
+ */
+function CellIconText({
+ icon,
+ iconPosition = 'leading',
+ color,
+ children,
+}: {
+ icon: React.ReactNode;
+ iconPosition?: 'leading' | 'trailing';
+ color: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {iconPosition === 'leading' && icon}
+
+ {children}
+
+ {iconPosition === 'trailing' && icon}
+
+ );
+}
+
+/** Sections where a missing item is better expressed as "Not Included"
+ * than a bare em-dash — these are opt-in items, so absence is meaningful. */
+const OPTIONAL_SECTION_HEADINGS = new Set(['Optionals', 'Extras']);
+
+function CellValue({
+ value,
+ sectionHeading,
+}: {
+ value: ComparisonCellValue;
+ sectionHeading: string;
+}) {
switch (value.type) {
case 'price':
return (
@@ -79,33 +127,31 @@ function CellValue({ value }: { value: ComparisonCellValue }) {
);
case 'complimentary':
return (
-
-
-
- Complimentary
-
-
+
+ }
+ >
+ Complimentary
+
);
case 'included':
return (
-
-
-
- Included
-
-
+
+ }
+ >
+ Included
+
);
case 'poa':
return (
@@ -115,20 +161,30 @@ function CellValue({ value }: { value: ComparisonCellValue }) {
);
case 'unknown':
return (
-
+
+ }
+ >
+ Unknown
+
+ );
+ case 'unavailable':
+ if (OPTIONAL_SECTION_HEADINGS.has(sectionHeading)) {
+ return (
- Unknown
+ Not Included
-
-
- );
- case 'unavailable':
+ );
+ }
return (
—
@@ -207,6 +263,19 @@ const tableSx = {
bgcolor: 'background.paper',
};
+/**
+ * Fixed column width for both the row-label column and each package column.
+ * Natural table width = COMPARISON_TABLE_COL_WIDTH × (packages.length + 1).
+ * Exposed so ComparisonPage can size its width-matching page header container
+ * to align left edges with the table on horizontal overflow.
+ */
+export const COMPARISON_TABLE_COL_WIDTH = 300;
+
+/** z-index scale for sticky layers inside the table. */
+const Z_HEADER_ROW = 30;
+const Z_STICKY_LEFT = 20;
+const Z_STICKY_LEFT_SECTION = 25; // section heading left cell above body cells
+
// ─── Component ──────────────────────────────────────────────────────────────
/**
@@ -219,10 +288,10 @@ const tableSx = {
*/
export const ComparisonTable = React.forwardRef(
({ packages, onArrange, onRemove, sx }, ref) => {
- const colCount = packages.length + 1;
const mergedSections = buildMergedSections(packages);
- const gridCols = `minmax(220px, 280px) repeat(${packages.length}, minmax(200px, 1fr))`;
- const minW = packages.length > 3 ? 960 : packages.length > 2 ? 800 : 600;
+ const colCount = packages.length + 1;
+ const gridCols = `${COMPARISON_TABLE_COL_WIDTH}px repeat(${packages.length}, ${COMPARISON_TABLE_COL_WIDTH}px)`;
+ const recommendedColIdx = packages.findIndex((p) => p.isRecommended);
return (
-
- {/* ── Package header cards ── */}
+ {/* ── Package header cards ── */}
+
+ {/* Info card — sticky-left, matches the row-label column below */}
- {/* Info card — stretches to match package card height, text at top */}
+
- {/* Package column header cards */}
- {packages.map((pkg) => (
+ {packages.map((pkg) => (
+
- ))}
-
+
+ ))}
+
- {/* ── Section tables (each separate with left accent headings) ── */}
- {mergedSections.map((section) => (
-
-
+ {/* ── Section tables (each separate with left accent headings) ── */}
+ {mergedSections.map((section) => (
+
+ {/* Section heading row — left cell sticky so label stays visible on horizontal scroll */}
+
+
{section.heading}
+ {/* Background continuation for the remaining columns so they
+ share the heading's surface-subtle wash. */}
+
+
- {section.items.map((item) => (
+ {section.items.map((item) => (
+
+ {/* Row-label cell — sticky-left */}
-
-
- {item.name}
-
- {item.info && (
-
- {'\u00A0'}
-
-
-
-
- )}
-
+
+ {item.name}
+
+ {item.info && (
+
+ {'\u00A0'}
+
+
+
+
+ )}
+
- {packages.map((pkg) => (
+ {packages.map((pkg, idx) => {
+ const isRecommended = idx === recommendedColIdx;
+ return (
-
+
- ))}
-
- ))}
-
- ))}
+ );
+ })}
+
+ ))}
+
+ ))}
- {packages.some((p) => p.itemizedAvailable === false) && mergedSections.length > 0 && (
-
- * Some providers have not provided an itemised pricing breakdown. Their items are
- shown as "—" above.
-
- )}
-
+ {packages.some((p) => p.itemizedAvailable === false) && mergedSections.length > 0 && (
+
+ * Some providers have not provided an itemised pricing breakdown. Their items are shown
+ as "—" above.
+
+ )}
);
},
diff --git a/src/components/organisms/ComparisonTable/index.ts b/src/components/organisms/ComparisonTable/index.ts
index f73c3ca..b972ba6 100644
--- a/src/components/organisms/ComparisonTable/index.ts
+++ b/src/components/organisms/ComparisonTable/index.ts
@@ -1,4 +1,4 @@
-export { ComparisonTable, default } from './ComparisonTable';
+export { ComparisonTable, COMPARISON_TABLE_COL_WIDTH, default } from './ComparisonTable';
export type {
ComparisonTableProps,
ComparisonPackage,
diff --git a/src/components/pages/ComparisonPage/ComparisonPage.stories.tsx b/src/components/pages/ComparisonPage/ComparisonPage.stories.tsx
index 0a6f8c1..78714e2 100644
--- a/src/components/pages/ComparisonPage/ComparisonPage.stories.tsx
+++ b/src/components/pages/ComparisonPage/ComparisonPage.stories.tsx
@@ -122,7 +122,7 @@ const pkgMackay: ComparisonPackage = {
name: 'Everyday Funeral Package',
price: 5495.45,
provider: {
- name: 'Mackay Family Funerals',
+ name: 'Mackay Family Funeral Directors & Cremation Services',
location: 'Inglewood',
logoUrl: DEMO_LOGO,
rating: 4.6,
diff --git a/src/components/pages/ComparisonPage/ComparisonPage.tsx b/src/components/pages/ComparisonPage/ComparisonPage.tsx
index 2183626..b72854a 100644
--- a/src/components/pages/ComparisonPage/ComparisonPage.tsx
+++ b/src/components/pages/ComparisonPage/ComparisonPage.tsx
@@ -1,14 +1,21 @@
import React, { useId, useState, useRef, useCallback } from 'react';
import Box from '@mui/material/Box';
+import Divider from '@mui/material/Divider';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import type { SxProps, Theme } from '@mui/material/styles';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
+import { Link } from '../../atoms/Link';
import { WizardLayout } from '../../templates/WizardLayout';
-import { ComparisonTable, type ComparisonPackage } from '../../organisms/ComparisonTable';
+import {
+ ComparisonTable,
+ COMPARISON_TABLE_COL_WIDTH,
+ type ComparisonPackage,
+} from '../../organisms/ComparisonTable';
import { ComparisonPackageCard } from '../../molecules/ComparisonPackageCard';
import { ComparisonTabCard } from '../../molecules/ComparisonTabCard';
@@ -113,27 +120,147 @@ export const ComparisonPage = React.forwardRef
- {/* Page header with Share/Print actions */}
-
-
-
+ {!isMobile && (
+ <>
+ {/* Page header zone — centred, bounded to the table's natural width */}
+
+
+
+
+ Back
+
+
+
+
+ Compare packages
+
+
+ {subtitle}
+
+
+
+ {(onShare || onPrint) && (
+
+ {onShare && (
+ }
+ onClick={onShare}
+ >
+ Share
+
+ )}
+ {onPrint && (
+ }
+ onClick={onPrint}
+ >
+ Print
+
+ )}
+
+ )}
+
+
+
+
+
+
+ {/* Table zone — width-matching spacers centre the table when room
+ allows, collapse to the minimum when table is wider than
+ viewport so overflow extends rightward from the page's
+ content column. */}
+
+
+
+
+
+
+
+ >
+ )}
+
+ {/* Mobile: Tab rail + card view */}
+ {isMobile && allPackages.length > 0 && (
+ <>
+
Compare packages
@@ -142,50 +269,21 @@ export const ComparisonPage = React.forwardRef
- {/* Share + Print */}
- {(onShare || onPrint) && (
-
- {onShare && (
- }
- onClick={onShare}
- >
- Share
-
- )}
- {onPrint && (
- }
- onClick={onPrint}
- >
- Print
-
- )}
-
- )}
-
-
+
- {/* Desktop: ComparisonTable */}
- {!isMobile && (
-
- )}
-
- {/* Mobile: Tab rail + card view */}
- {isMobile && allPackages.length > 0 && (
- <>
- {/* Tab rail — mini cards showing provider + package + price */}
+
+ Choose a package to view
+
+ {/* Dot indicator — position + count. Purely visual supplement;
+ the tab rail above is the accessible navigation, so dots
+ are aria-hidden and skipped by keyboard tab-order. */}
+
+ {allPackages.map((_, idx) => {
+ const isActive = idx === activeTabIdx;
+ return (
+ handleTabClick(idx)}
+ sx={{
+ appearance: 'none',
+ border: 0,
+ background: 'transparent',
+ cursor: 'pointer',
+ p: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ '& > span': {
+ display: 'block',
+ width: isActive ? 24 : 8,
+ height: 8,
+ borderRadius: 4,
+ bgcolor: isActive
+ ? 'var(--fa-color-brand-600)'
+ : 'var(--fa-color-neutral-300)',
+ transition: 'width 0.2s ease, background-color 0.2s ease',
+ },
+ }}
+ >
+
+
+ );
+ })}
+
+
{activePackage && (
);
+/** Bleed: full-width scroll host. Main becomes the single scroll container
+ * (both axes). No inner Container — children are full-bleed. Back link is
+ * passed into children so it scrolls with the page content. Used by pages
+ * that own their own width + alignment logic (e.g. ComparisonPage). */
+const BleedLayout: React.FC<{
+ children: React.ReactNode;
+ backLink?: React.ReactNode;
+}> = ({ children, backLink }) => (
+
+ {backLink}
+ {children}
+
+);
+
// ─── Variant map ─────────────────────────────────────────────────────────────
const LAYOUT_MAP: Record<
@@ -378,6 +403,7 @@ const LAYOUT_MAP: Record<
'list-detail': ListDetailLayout,
'grid-sidebar': GridSidebarLayout,
'detail-toggles': DetailTogglesLayout,
+ bleed: BleedLayout,
};
/* Stepper bar renders on any variant when progressStepper or runningTotal is provided */
@@ -387,12 +413,15 @@ const LAYOUT_MAP: Record<
/**
* Page-level layout template for the FA arrangement wizard.
*
- * Provides 5 layout variants matching the wizard page templates:
+ * Provides 6 layout variants matching the wizard page templates:
* - **centered-form**: Single centered column for form steps (intro, auth, date/time, etc.)
+ * - **wide-form**: Wider single column for card grids (coffins, etc.)
* - **list-map**: Split view with scrollable card list and map panel (providers)
* - **list-detail**: Master-detail split for selection + detail (packages, preview)
* - **grid-sidebar**: Filter sidebar + card grid (coffins)
* - **detail-toggles**: Hero image + info column (venue, coffin details)
+ * - **bleed**: Viewport-locked, full-width scroll host with no inner container —
+ * the page owns its own alignment (comparison page)
*
* All variants share: navigation slot, optional back link, sticky help bar,
* and optional progress stepper + running total bar (shown when props provided).
@@ -426,8 +455,8 @@ export const WizardLayout = React.forwardRef(
flexDirection: 'column',
minHeight: '100vh',
bgcolor: 'background.default',
- // list-map + detail-toggles: lock to viewport so panels scroll independently
- ...((variant === 'list-map' || variant === 'detail-toggles') && {
+ // list-map + detail-toggles + bleed: lock to viewport so panels scroll independently
+ ...((variant === 'list-map' || variant === 'detail-toggles' || variant === 'bleed') && {
height: '100vh',
overflow: 'hidden',
}),
@@ -445,15 +474,19 @@ export const WizardLayout = React.forwardRef(
{/* Stepper + running total bar (grid-sidebar, detail-toggles only) */}
- {/* Back link — inside left panel for list-map/detail-toggles, above content for others */}
- {showBackLink && variant !== 'list-map' && variant !== 'detail-toggles' && (
-
-
-
- )}
+ {/* Back link — inside children for list-map/detail-toggles/bleed (scrolls with content),
+ above content for other variants */}
+ {showBackLink &&
+ variant !== 'list-map' &&
+ variant !== 'detail-toggles' &&
+ variant !== 'bleed' && (
+
+
+
+ )}
{/* Main content area */}
(