// Amenities — three-column hi-fi viewer, replicated from the Embassy Citadel
// amenities layout (the stronger of our two reference builds) and re-skinned in
// The Universe's ivory/gold brand on this app's React/Babel stack.
//
// REFERENCE LAYOUT (Embassy Citadel · v4/amenities.html):
// · LEFT editorial rail — eyebrow + big serif "Amenities" + tagline + rule,
// then a category list (icon · label · NN count) where one row is active,
// and a LEVEL filter beneath it.
// · CENTER column — one large feature HERO plate (the active item's
// render, or the group hero) with a soft gradient + caption, and a floating
// prev / NN—NN counter / next control pill overlaid at the foot.
// · RIGHT rail — a vertical THUMBNAIL STACK of every item in the
// active category (photo + name + level), active thumb carries a gold ring;
// scrolls when the group has many items.
//
// Citadel chosen over Embassy One because its 3-column category-rail + hero +
// thumb-stack maps 1:1 onto our data (5 experience groups · 33 items · 4 levels)
// and is the richer, more catalogue-forward pattern. We use a RECTANGULAR hero
// plate (not Citadel's circle) — our client renders are wide 16:9 and read far
// better un-cropped in a rectangle.
//
// Data: AMENITY_GROUPS (5 groups · 33 items) · AMENITY_COUNT (33). Image paths
// assets/renders/client/
.jpg and .jpg. Falls back to the group hero
// on any per-item image error. Ends with window.Amenities = Amenities.
const AM_KEYS_ID = 'uni-amenities-keys';
function ensureAmKeys() {
if (typeof document === 'undefined') return;
if (document.getElementById(AM_KEYS_ID)) return;
const s = document.createElement('style');
s.id = AM_KEYS_ID;
s.textContent = `
@keyframes amHeroIn {
0% { opacity: 0; transform: scale(1.05); }
100% { opacity: 1; transform: scale(1.02); }
}
@keyframes amThumbIn {
0% { opacity: 0; transform: translateX(14px); }
100% { opacity: 1; transform: translateX(0); }
}
.am-stack::-webkit-scrollbar { width: 8px; }
.am-stack::-webkit-scrollbar-track { background: transparent; }
.am-stack::-webkit-scrollbar-thumb { background: rgba(176,138,63,0.30); border-radius: 4px; }
.am-stack::-webkit-scrollbar-thumb:hover { background: rgba(176,138,63,0.55); }
`;
document.head.appendChild(s);
}
const AM_R = (name) => `assets/renders/client/${name}.jpg`;
const AM_LEVELS = ['All', 'GF', 'Clubhouse', 'Podium', 'L2'];
const AM_LEVEL_LABEL = { All:'All Levels', GF:'Ground Floor', Clubhouse:'Clubhouse', Podium:'Podium', L2:'Level 2' };
// One-line editorial descriptor per experience group (Frank-Curator register).
const AM_DESC = {
sport: 'A podium of courts, pitches and a running track for recreation at every age.',
wellness: 'An Olympic-grade pool, jacuzzi, a clubhouse gym and a yoga & crossfit floor.',
social: 'Banquet, café, library, games and guest rooms, interiors curated by HBA.',
family: 'A crèche, kids’ water play and a dedicated children’s playground, safe by design.',
landscape: 'Acres of landscaping by SWA: water features, seating gardens and accent groves.',
};
// ============================================================================
// AmImg —
that falls back to the group hero render on load error.
// ============================================================================
function AmImg({ src, fallback, alt, style, className }) {
const triedRef = React.useRef(false);
React.useEffect(() => { triedRef.current = false; }, [src]);
return (
{
if (!triedRef.current && fallback && ev.currentTarget.src.indexOf(fallback) === -1) {
triedRef.current = true;
ev.currentTarget.src = fallback;
} else {
ev.currentTarget.style.opacity = '0';
}
}}
/>
);
}
// ============================================================================
// AmCategoryRow — left-rail entry: icon · label · count, active = gold tint.
// NOTE: namespaced (Am-) to avoid colliding with gallery.jsx's own top-level
// `CategoryRow` — both screens are