// Home — split-stage cosmos. // // · LEFT — editorial title block. Big "Center of everything." headline, // descriptive paragraph, key stats inline, ambient cosmic backdrop. // · RIGHT — 8 module buttons orbit a smaller centre circle. The centre // circle holds the U monogram (no text), with the 10-floor apartment // sketch animating behind it. // · Tap a button → its icon JUMPS UP, FLIPS in the air (rotateY card-flip), // then FLIES into the centre circle and lands with a flash. After landing // the page transitions to that screen. // · Cosmos: warm-gold dust particles drifting on a light substrate. const HOME_KEYS_ID = 'uni-home-split-keys'; function ensureHomeKeys() { if (typeof document === 'undefined') return; if (document.getElementById(HOME_KEYS_ID)) return; const s = document.createElement('style'); s.id = HOME_KEYS_ID; s.textContent = ` /* The icon's flight: anticipation jump → flip → glide to centre. Per-instance --dx/--dy are set on click for the centre vector. Uses rotateZ (always visible) instead of rotateY (disappears edge-on), and ends at scale 0.55 so the landing read clearly. */ @keyframes uniIconJumpFlipFly { 0% { transform: translate(0,0) translateY(0) scale(1) rotate(0deg); filter: drop-shadow(0 0 0 transparent); opacity: 1; } 25% { transform: translate(0,0) translateY(-60px) scale(1.25) rotate(90deg); filter: drop-shadow(0 18px 30px rgba(232,215,168,0.6)) drop-shadow(0 0 22px rgba(255,238,180,0.75)); opacity: 1; } 70% { transform: translate(calc(var(--dx,0) * 0.7), calc(var(--dy,0) * 0.7 - 30px)) scale(1.30) rotate(360deg); filter: drop-shadow(0 0 42px rgba(255,238,180,1.0)) drop-shadow(0 0 22px rgba(255,238,180,0.9)); opacity: 1; } 100% { transform: translate(var(--dx,0), var(--dy,0)) scale(0.55) rotate(540deg); filter: drop-shadow(0 0 20px rgba(255,238,180,0.6)); opacity: 0; } } /* gold halo disk that carries the icon during flight — keeps the icon legible against any background */ @keyframes uniIconHalo { 0% { transform: scale(0); opacity: 0; } 18% { transform: scale(1); opacity: 0.95; } 85% { transform: scale(1); opacity: 0.85; } 100% { transform: scale(0.6); opacity: 0; } } @keyframes uniSatBlast { 0% { box-shadow: 0 18px 36px rgba(50,32,12,0.20), 0 8px 16px rgba(50,32,12,0.12), inset 0 1px 0 rgba(255,246,224,0.16); transform: scale(1); } 28% { box-shadow: 0 0 0 14px rgba(216,181,115,0.18), 0 0 80px 18px rgba(232,216,179,0.45), inset 0 0 50px rgba(232,216,179,0.18); transform: scale(1.06); } 62% { box-shadow: 0 0 0 22px rgba(216,181,115,0), 0 0 110px 26px rgba(232,216,179,0.18); transform: scale(0.96); } 100% { transform: scale(1); } } @keyframes uniCenterFlash { 0% { background: radial-gradient(circle, rgba(255,238,180,0) 0%, rgba(232,215,168,0) 60%); transform: scale(1); } 28% { background: radial-gradient(circle, rgba(255,238,180,0.95) 0%, rgba(232,215,168,0.55) 38%, rgba(232,215,168,0) 70%); transform: scale(1.08); } 100% { background: radial-gradient(circle, rgba(255,238,180,0) 0%, rgba(232,215,168,0) 60%); transform: scale(1.22); } } @keyframes uniCenterRing { 0% { transform: scale(0.55); opacity: 0.95; border-width: 3px; } 100% { transform: scale(2.6); opacity: 0; border-width: 1px; } } /* Settling wave — fired when arriving from /explore. A ring expands from the centre out past the screen edges; subtle, not heavy. 3 instances are rendered with staggered delays for depth. */ @keyframes uniSettleWave { 0% { transform: translate(-50%, -50%) scale(0.40); opacity: 0.85; border-width: 3px; box-shadow: 0 0 22px rgba(255,238,180,0.45); } 18% { opacity: 0.78; } 100% { transform: translate(-50%, -50%) scale(20); opacity: 0; border-width: 0.4px; box-shadow: 0 0 6px rgba(255,238,180,0); } } /* Cinematic text reveal — blur-to-clear + soft lift. */ @keyframes uniCinematicIn { 0% { opacity: 0; transform: translateY(14px); filter: blur(10px); } 100% { opacity: 1; transform: translateY(0); filter: blur(0); } } /* Mini-CRM panel — slides in from the left edge. */ @keyframes uniCrmPanelIn { 0% { opacity: 0; transform: translateX(-46px); } 100% { opacity: 1; transform: translateX(0); } } /* ── Building backdrop motion ────────────────────────────── Reveal: the twin-tower drawing "inks in" from the ground up on load. Float: a slow horizontal parallax drift (stays within the side margins so the building is never cropped top/bottom). Sweep: a golden light band passes across the towers. Glow: a faint contrast breathe. */ @keyframes uniBldgReveal { from { opacity:0; clip-path: inset(100% 0 0 0); } to { opacity:1; clip-path: inset(0 0 0 0); } } @keyframes uniBldgFloat { 0%{ transform: translateX(0); } 50%{ transform: translateX(-22px); } 100%{ transform: translateX(0); } } @keyframes uniBldgSweep { 0%{ transform: translateX(-140%); opacity:0; } 12%{ opacity:1; } 88%{ opacity:1; } 100%{ transform: translateX(360%); opacity:0; } } @keyframes uniBldgGlow { 0%,100%{ opacity:0.52; } 50%{ opacity:0.62; } } /* Constrain the 100x100 SVG glyphs inside their orbital button */ .uni-sat-icon svg { width: 100% !important; height: 100% !important; display: block; } /* Mini-CRM scroll areas — slim gold scrollbar */ .uni-crm-scroll::-webkit-scrollbar { width: 7px; } .uni-crm-scroll::-webkit-scrollbar-thumb { background: rgba(176,138,63,0.30); border-radius: 7px; } .uni-crm-scroll::-webkit-scrollbar-track { background: transparent; } .uni-crm-hscroll::-webkit-scrollbar { height: 7px; } .uni-crm-hscroll::-webkit-scrollbar-thumb { background: rgba(176,138,63,0.30); border-radius: 7px; } .uni-crm-hscroll::-webkit-scrollbar-track { background: transparent; } `; document.head.appendChild(s); } // ============================================================================ // Geometry — split layout // ============================================================================ // LEFT block sits in the left half of the canvas; RIGHT cluster sits at the // right. Sized so it reads strongly when the 2560×1600 canvas is scaled // down to a typical tablet — tiles, monogram, and label type are all // generous. const RIGHT_CX = 1880; const RIGHT_CY = 830; const SAT_R = 460; const TILE_SIZE = 172; const CIRCLE_R = 200; // centre circle holds the U monogram // Mini-CRM panel — occupies the LEFT 60% of the 2560-wide canvas; the centre // circle (right 40%) stays visible as the "centre button" while it's open. const CRM_PANEL_W = 1536; // 60% of 2560 const CRM_PAD = 56; // Pixel delta between explore-CX (1280) and home-RIGHT_CX (1880) — used to // glide the cluster from the explore-centre position to the home-right // position when arriving via the from-explore handoff. const HOME_SLIDE_X = 600; function Home() { ensureHomeKeys(); const t = useLoop(); const [hovered, setHovered] = React.useState(null); const [jumping, setJumping] = React.useState(null); const [exiting, setExiting] = React.useState(false); const [centerLand, setCenterLand] = React.useState(0); const [ripples, fireRipple] = useRipple(1200); const [bursts, fireBurst] = useBurst(900); const modules = PROJECT.modules; // ── Canvas-height density. 0 on the primary 16:10 tablet (H=1600, Tab S7 — // mathematically UNCHANGED) → 1 on iPad Pro 4:3 (H=1920). Everything below // keys off `dens` so the cluster, type and the new bottom fact-bar grow to // fill the taller 4:3 canvas instead of leaving a dead gap underneath. const H = (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600; const dens = clamp((H - 1600) / 320); // Drop the cluster centre down so it tracks the auto-centred left text block // (which sits at 50% of H) instead of floating high. Grows the orbit radius, // tile size and centre circle so the whole assembly reads bigger on iPad. const RCY = Math.round(RIGHT_CY + dens * 122); // 830 → ~952 const SR = Math.round(SAT_R + dens * 46); // 460 → ~506 const TS = Math.round(TILE_SIZE + dens * 22); // 172 → ~194 const circleScale = 1 + dens * 0.12; // centre circle / monogram const innerR = CIRCLE_R * circleScale + 18; const outerR = SR - TS / 2 - 12; const labelFS = Math.round(27 + dens * 4); const hotR = Math.round(CIRCLE_R * circleScale); // ── Hidden Sales-Desk mini-CRM. Triple-tapping the centre circle (3 taps // within ~600ms) toggles it open. The centre circle has no single-tap // action of its own, so a lone tap is harmlessly absorbed by the counter. const [crmOpen, setCrmOpen] = React.useState(false); const tapRef = React.useRef({ n: 0, timer: null }); const handleCenterTap = () => { const s = tapRef.current; s.n += 1; if (s.timer) clearTimeout(s.timer); if (s.n >= 3) { s.n = 0; setCrmOpen(o => !o); return; } s.timer = setTimeout(() => { s.n = 0; }, 600); }; // Three entry modes: // fromExplore — arrived seamlessly from /explore. Orbital + circle are // already in their final positions. A settling wave rolls // out from the centre, then the left-block text reveals // cinematically (blur-to-clear stagger). // firstVisit — full cinematic from-zero entrance (legacy first-load). // return — back from a sub-screen. Fast-forward. const fromExplore = React.useRef( typeof sessionStorage !== 'undefined' && sessionStorage.getItem('uni-from-explore') === '1' ).current; const isFirstVisit = React.useRef( typeof sessionStorage === 'undefined' || !sessionStorage.getItem('uni-home-seen') ).current; React.useEffect(() => { if (typeof sessionStorage !== 'undefined') { sessionStorage.setItem('uni-home-seen', '1'); sessionStorage.removeItem('uni-from-explore'); } }, []); // Cluster slide: when fromExplore, paint at translateX(-HOME_SLIDE_X) on the // first frame, then on the SECOND frame flip `slid` to true so the CSS // transition kicks in. Double-rAF guarantees the browser commits the start // position to the GPU before applying the end position. const [slid, setSlid] = React.useState(!fromExplore); // The settling wave (the "ripple") only mounts AFTER the slide finishes. // Otherwise the rings sit visibly at the centre while the cluster is still // sliding — which reads as a flicker right before they expand outward. const [waveReady, setWaveReady] = React.useState(false); React.useEffect(() => { if (!fromExplore) return; const r1 = requestAnimationFrame(() => { const r2 = requestAnimationFrame(() => setSlid(true)); // store r2 cleanup via outer closure return () => cancelAnimationFrame(r2); }); // Slide is 820ms. Mount the wave a hair after to guarantee the cluster // has fully settled before the rings begin expanding. const wt = setTimeout(() => setWaveReady(true), 860); return () => { cancelAnimationFrame(r1); clearTimeout(wt); }; }, []); // Phase clocks — branch on entry mode. let titleP, paraP, statsP, circleP, lineP, buildingP, orbitalStart, eT; if (fromExplore) { // Orbital + circle + building are already settled (matches how /explore // left them). Only the LEFT text block needs to fade in — and we delay // it slightly so the user feels the settling wave first. eT = t + 2.6; // pretend the entry-time has elapsed circleP = 1; // circle fully drawn lineP = 1; // underline fully drawn buildingP = 1; // apartment fully revealed orbitalStart = -100; // tiles all show at e=1 from t=0 // Cinematic text — first wave plays t=0..0.9, then text from t=0.7 // Slide takes ~820ms; defer text reveal so it lands AFTER the cluster // has finished gliding into place. titleP = clamp((t - 0.95) / 1.05); paraP = clamp((t - 1.35) / 0.95); statsP = clamp((t - 1.80) / 0.95); } else { const tOffset = isFirstVisit ? 0 : 1.6; eT = t + tOffset; titleP = clamp((eT - 0.2) / 0.9); paraP = clamp((eT - 0.9) / 0.7); statsP = clamp((eT - 1.4) / 0.8); circleP = clamp((eT - 0.8) / 1.0); orbitalStart = 1.2; buildingP = clamp((eT - 2.0) / 1.0); lineP = clamp((eT - 1.6) / 0.7); } // Click handler — set --dx/--dy then fire the keyframe. const handleSatelliteClick = (ev, m, i, x, y) => { if (jumping) return; const root = document.querySelector('.tablet-bezel').getBoundingClientRect(); const scale = root.width / 2560; const tapX = (ev.clientX - root.left) / scale; const tapY = (ev.clientY - root.top) / scale; fireRipple(tapX, tapY); fireBurst(x, y, { count: 11 }); const iconEl = ev.currentTarget.querySelector('.uni-sat-icon'); if (iconEl) { iconEl.style.setProperty('--dx', (RIGHT_CX - x) + 'px'); iconEl.style.setProperty('--dy', (RCY - y) + 'px'); } setJumping(m.id); // Landing flash near the end of the icon flight (~440ms in) setTimeout(() => setCenterLand(c => c + 1), 440); setTimeout(() => setExiting(true), 520); setTimeout(() => navigate(m.id === 'masterplan' ? 'masterplan-explorer' : m.id), 640); }; return (
{/* The twin-tower drawing is painted at the VIEWPORT level (see index.html HOME_BG) so it's edge-to-edge with no cut at any aspect. Here we only add a soft left cream scrim so the "Center of everything." headline stays crisp over the lighter parts of the drawing. */}
{/* === COSMIC DUST BACKDROP === */} {/* warm radial overlay favours the right (where the cluster lives) */}
{/* === SETTLING WAVE — three concentric rings emanate from the centre AFTER the cluster glides into place. We gate mounting on `waveReady` (set ~860ms after mount) so the rings don't sit visibly at the centre during the slide — they begin expanding the instant they appear, no held-start-state flicker. */} {fromExplore && waveReady && (
{[0, 1, 2].map(i => (
))}
)} {/* === TOP BAR (hidden while the mini-CRM is open) === */} {!crmOpen &&
navigate('splash')} onKeyDown={ev=>{ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); navigate('splash'); } }} onMouseEnter={ev=>{ ev.currentTarget.style.opacity='0.6'; ev.currentTarget.style.transform='scale(1.03)'; }} onMouseLeave={ev=>{ ev.currentTarget.style.opacity='1'; ev.currentTarget.style.transform='scale(1)'; }} style={{display:'flex', alignItems:'center', gap:23, cursor:'pointer', transition:'opacity 200ms ease, transform 200ms ease', transformOrigin:'left center'}}>
RERA · {PROJECT.rera.split('/').slice(-2).join('/')}
{new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}).toUpperCase()}
} {/* === LEFT BLOCK — editorial title + paragraph + stats (hidden while CRM open) === */} {!crmOpen && } {/* === BOTTOM FACT BAR — fills the extra 4:3 canvas height with a refined row of real project facts. Fades in with `dens` so the primary 16:10 tablet (dens 0) is untouched; full presence on iPad Pro. === */} {!crmOpen && } {/* === CLUSTER WRAPPER ============================================ When arriving from /explore, the cluster (spokes + centre circle + 8 pieces) starts SHIFTED LEFT by 600px — the exact delta from home's RIGHT_CX (1880) back to explore's CX (1280) — so it sits in the same screen position the explore page just left it. After mount we flip a state and CSS-transition translateX(0) over 820ms, gliding the cluster across to its home location. */}
{/* === CONSTELLATION SPOKES (hidden while the mini-CRM is open) === */} {!crmOpen && {modules.map((m, i) => { const angle = (i * Math.PI/4) - Math.PI/2; const innerX = RIGHT_CX + innerR * Math.cos(angle); const innerY = RCY + innerR * Math.sin(angle); const outerX = RIGHT_CX + outerR * Math.cos(angle); const outerY = RCY + outerR * Math.sin(angle); const focused = hovered === m.id || jumping === m.id; const baseOp = lineP * (focused ? 0.85 : 0.30); const phase = ((t * (focused ? 0.9 : 0.30)) % 1); const ex = innerX + (outerX - innerX) * phase; const ey = innerY + (outerY - innerY) * phase; return ( {focused && ( )} ); })} } {/* === RIGHT CLUSTER — centre circle (logo) always shown; it IS the "centre button" that remains when the orbital tiles disappear === */} {/* HIDDEN CRM TRIGGER — invisible circular hotspot over the centre circle. Triple-tap toggles the Sales-Desk console. No visible affordance; single taps do nothing (absorbed by the tap counter). */}
{!crmOpen && modules.map((m, i) => { const angle = (i * Math.PI/4) - Math.PI/2; const x = RIGHT_CX + SAT_R * Math.cos(angle); const y = RIGHT_CY + SAT_R * Math.sin(angle); const start = orbitalStart + i * 0.10; const local = clamp((eT - start) / 0.7); const e = ease.outBack(local); const isHovered = hovered === m.id; const isJumping = jumping === m.id; const isOtherJumping = jumping && jumping !== m.id; const dxIn = (RIGHT_CX - x) * (1 - e) * 0.42; const dyIn = (RIGHT_CY - y) * (1 - e) * 0.42; return (
setHovered(m.id)} onMouseLeave={()=>setHovered(null)} onClick={(ev)=>handleSatelliteClick(ev, m, i, x, y)} /> {/* compact label */}
{m.label}
); })}
{/* === END CLUSTER WRAPPER === */} {/* === HIDDEN SALES-DESK CONSOLE (mini-CRM) === */} {crmOpen && setCrmOpen(false)}/>}
); } // ============================================================================ // LEFT block — editorial headline + paragraph + stat grid + scan hint // ============================================================================ function LeftTitleBlock({ t, eT, titleP, paraP, statsP }) { const cursorOn = (Math.sin(t * 9) > 0) && titleP > 0.95 && titleP < 1; // Personalised welcome — when a walk-in is selected in the mini-CRM the home // greets them by name, so the tablet handed to the customer feels curated. const cust = window.UNI_SESSION && window.UNI_SESSION.getCustomer(); return (
{/* eyebrow — greets the selected walk-in, else the standard locator line */}
{cust ? `WELCOME · ${cust.name.toUpperCase()}` : 'THE UNIVERSE · NEHRU NAGAR · AHMEDABAD'}
{cust && (
Your private viewing of The Universe begins here.
)} {/* headline — two lines, "everything" italic gold */}

Center of everything. {cursorOn && ( )}

{/* underline */} {/* paragraph */}
7 acres in the gravitational pull of Ahmedabad, with everything that matters orbiting at walking distance: the schools, the hospitals, the highway, the airport, and the living rooms of friends you haven't met yet.
{/* stats strip — single airy horizontal row, footnote band */}
{[ { k: 'TOTAL LAND', v: '7', sub: 'acres' }, { k: 'TOWERS', v: '10', sub: 'high-rise' }, { k: 'PODIUM', v: '4', sub: 'acre garden' }, { k: 'TYPOLOGY', v: '4 BHK', sub: 'plus penthouse' }, ].map((s, i) => ( {i > 0 && (
)}
{s.k}
{s.v}
{s.sub}
))}
{/* scan-the-orbit hint */}
EXPLORE THE ORBIT
); } // ============================================================================ // CentreCircle — logo monogram inside the cluster, with apartment sketch // ============================================================================ function CentreCircle({ t, circleP, buildingP, centerLand }) { const D = CIRCLE_R * 2; return (
{/* halo */}
{/* concentric rings */} {/* slowly-rotating dotted outer ring */} {/* 8 tick marks at the satellite compass positions */} {Array.from({length:8}).map((_, i) => { const a = (i * Math.PI/4) - Math.PI/2; const r1 = CIRCLE_R + 4, r2 = CIRCLE_R + 12; const x1 = D/2 + 28 + r1*Math.cos(a), y1 = D/2 + 28 + r1*Math.sin(a); const x2 = D/2 + 28 + r2*Math.cos(a), y2 = D/2 + 28 + r2*Math.sin(a); return ; })} {/* apartment sketch — inside circle, behind monogram */}
{/* LANDING FLASH + ring expansion when an icon arrives */} {centerLand > 0 && (
{[0,1,2].map(i => (
))} )} {/* The U monogram — focal mark */}
); } // ============================================================================ // SatelliteButton — round orbital tile // ============================================================================ function SatelliteButton({ m, i, isHovered, isJumping, onClick, onMouseEnter, onMouseLeave, t }) { const Glyph = UIcons[m.id] || UIcons.story; const active = isHovered || isJumping; return (
{/* ambient halo when active */} {active && (
)} {/* keystone tick */}
{/* number badge */}
0{i + 1}
{/* glyph — flies on tap (jump → spin → glide to centre). A gold halo "coin" rides under the icon during flight so it never disappears against a busy background. */}
{isJumping && (
)}
{/* base hairline */}
); } // ============================================================================ // CosmicDust — light-theme cosmos backdrop // ============================================================================ function CosmicDust({ count = 110, opacity = 0.7 }) { const ref = React.useRef(null); const rafRef = React.useRef(0); React.useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; let W = 0, H = 0; const resize = () => { const r = canvas.getBoundingClientRect(); W = r.width; H = r.height; canvas.width = W * dpr; canvas.height = H * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); window.addEventListener('resize', resize); const rng = (seed => () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 0xffffffff)(0xc05a1f); const dust = Array.from({ length: count }, () => { const isPlanet = rng() < 0.06; return { ax: rng() * (W || 800), ay: rng() * (H || 600), oa: rng() * Math.PI * 2, orbR: 8 + rng() * 32, os: 0.0006 + rng() * 0.0014, r: isPlanet ? 2.4 + rng() * 4.0 : 0.6 + rng() * 1.6, twink: rng() * Math.PI * 2, twinkS: 0.005 + rng() * 0.018, hue: rng() < 0.55 ? 'gold' : 'cream', baseAlpha: isPlanet ? 0.40 : 0.55 + rng() * 0.40, isPlanet, }; }); let last = performance.now(); const tick = (now) => { const dt = Math.min(50, now - last); last = now; ctx.clearRect(0, 0, W, H); for (const p of dust) { p.oa += p.os * dt; p.twink += p.twinkS * dt; const x = p.ax + p.orbR * Math.cos(p.oa); const y = p.ay + p.orbR * Math.sin(p.oa); const a = p.baseAlpha * (0.45 + 0.55 * Math.sin(p.twink)) * opacity; const baseCol = p.hue === 'gold' ? '176,138,63' : '232,215,168'; if (p.isPlanet) { const grd = ctx.createRadialGradient(x, y, 0, x, y, p.r * 5); grd.addColorStop(0, `rgba(${baseCol},${a})`); grd.addColorStop(0.45, `rgba(${baseCol},${a*0.28})`); grd.addColorStop(1, `rgba(${baseCol},0)`); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(x, y, p.r * 5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = `rgba(${baseCol},${Math.min(1, a * 1.4)})`; ctx.beginPath(); ctx.arc(x, y, p.r, 0, Math.PI*2); ctx.fill(); } else { ctx.fillStyle = `rgba(${baseCol},${a})`; ctx.beginPath(); ctx.arc(x, y, p.r, 0, Math.PI*2); ctx.fill(); } } rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => { cancelAnimationFrame(rafRef.current); window.removeEventListener('resize', resize); }; }, [count, opacity]); return ; } // ============================================================================ // Apartment10 — 10-floor apartment sketch (slow construction loop) // ============================================================================ function Apartment10({ t, period = 22, color = 'var(--gold-deep)' }) { const phase = (t / period) % 1; const fadeP = 1 - clamp((phase - 0.86) / 0.14); const SEG_DUR = 0.07; const reveal = (s) => ease.outQuart(clamp((phase - s) / SEG_DUR)); const floors = [80, 120, 160, 200, 240, 280, 320, 360, 400, 440]; return ( {floors.slice().reverse().map((y, i) => ( ))} {[ { x: 92, y: 88, o: 0.68 }, { x: 152, y: 168, o: 0.55 }, { x: 272, y: 248, o: 0.62 }, { x: 212, y: 328, o: 0.75 }, { x: 92, y: 408, o: 0.5 }, { x: 272, y: 128, o: 0.6 }, ].map((w, i) => ( ))} ); } // ============================================================================ // LiveDot — top-right pulsing live indicator // ============================================================================ function LiveDot({ t }) { const pulse = 0.5 + 0.5 * Math.sin(t * Math.PI * 2); return (
LIVE INVENTORY
); } // ============================================================================ // 8 BESPOKE GLYPHS — light cream on dark beige tile body (unchanged) // ============================================================================ const Sw = 1.25; const Glow = (extra={}) => ({ fill:'none', stroke:'currentColor', strokeWidth: Sw, strokeLinecap:'round', strokeLinejoin:'round', ...extra, }); const UIcons = { story: ({ hovered, t }) => ( ), location: ({ hovered, t }) => ( ), masterplan: ({ hovered, t }) => ( ), residences: ({ hovered, t }) => ( ), amenities: ({ hovered, t }) => ( ), gallery: ({ hovered, t }) => ( ), tools: ({ hovered, t }) => ( ), booking: ({ hovered, t }) => ( ), }; // ============================================================================ // SALES DESK — hidden mini-CRM console (triple-tap the centre circle to open) // ============================================================================ // Full-canvas cream/gold takeover. Header (project eyebrow + SALES DESK title + // today's stats), a left WALK-INS list with stage filters, and a right detail // pane for the active walk-in: preferences → recommended matches → journey. // Everything is sized to the 2560×1600 canvas with NO page scroll; overflow is // pushed into in-canvas glass popups. // ── Seed data (guarded so it only defines once) ───────────────────────────── if (typeof window !== 'undefined' && !window.SALES_DESK) { window.SALES_DESK = { stats: { walkInsToday: 5, awaitingSales: 1, liveNow: 3 }, stages: [ { key:'all', label:'All', count:5, dot:'var(--gold)' }, { key:'awaiting', label:'Awaiting sales', count:1, dot:'var(--venus-red)' }, { key:'withsales', label:'With sales', count:1, dot:'var(--gold-deep)' }, { key:'browsing', label:'Browsing', count:1, dot:'#7c8a6b' }, { key:'negotiating', label:'Negotiating', count:1, dot:'#c98a3b' }, { key:'followup', label:'Follow-up', count:1, dot:'#8a7bb2' }, { key:'closed', label:'Closed', count:0, dot:'#4f9d6a' }, ], walkIns: [ { id:'WI-2048', name:'Rohan Jain', initials:'RJ', stage:'withsales', config:'4 BHK · High floor', budget:'₹6.5–7.2 Cr', intent:'End-use + investment', agent:'Meera K.', ago:'4 min ago', phone:'+91 98250 11020', email:'rohan.jain@gmail.com', matchScore:94, session:'Live · 12 min', prefs:{ typology:'4 BHK', sqft:'3,400–3,600 sq.ft', budget:'₹6.5–7.2 Cr', purpose:'End-use', timeline:'This quarter', family:'4 — two children', source:'Referral · existing owner', mustHaves:['Riverfront view','High floor (20+)','Private elevator lobby','Corner unit'] }, matches:[ { no:'E-2104', tower:'Meridian', floor:'21st', bhk:'4 BHK', view:'Riverfront', sqft:'3,420 sq.ft', price:'₹6.84 Cr', match:96 }, { no:'E-1903', tower:'Meridian', floor:'19th', bhk:'4 BHK', view:'Riverfront', sqft:'3,420 sq.ft', price:'₹6.61 Cr', match:92 }, { no:'C-2201', tower:'Celeste', floor:'22nd', bhk:'4 BHK', view:'City skyline', sqft:'3,510 sq.ft', price:'₹7.05 Cr', match:88 }, { no:'C-2002', tower:'Celeste', floor:'20th', bhk:'4 BHK', view:'Podium garden', sqft:'3,360 sq.ft', price:'₹6.40 Cr', match:84 }, ], journey:[ { label:'Walk-in registered', at:'11:58 AM', done:true, current:false }, { label:'Profile captured', at:'12:03 PM', done:true, current:false }, { label:'Browsing units', at:'12:06 PM', done:true, current:false }, { label:'With sales — Meera K.', at:'12:09 PM', done:false, current:true }, { label:'Negotiation', at:'—', done:false, current:false }, { label:'Booking', at:'—', done:false, current:false }, ], }, { id:'WI-2049', name:'Aisha Khan', initials:'AK', stage:'awaiting', config:'3 BHK · Mid floor', budget:'₹4.2–4.8 Cr', intent:'First home', agent:null, ago:'2 min ago', phone:'+91 99745 33218', email:'aisha.khan@outlook.com', matchScore:81, session:'Awaiting · 2 min', prefs:{ typology:'3 BHK', sqft:'2,580–2,650 sq.ft', budget:'₹4.2–4.8 Cr', purpose:'First home', timeline:'Next 6 months', family:'3 — one child', source:'Walk-in · hoarding', mustHaves:['Vastu compliant','Garden view','Covered parking ×2'] }, matches:[ { no:'A-1402', tower:'Aurora', floor:'14th', bhk:'3 BHK', view:'Podium garden', sqft:'2,610 sq.ft', price:'₹4.35 Cr', match:90 }, { no:'A-1605', tower:'Aurora', floor:'16th', bhk:'3 BHK', view:'Garden + city', sqft:'2,640 sq.ft', price:'₹4.58 Cr', match:86 }, { no:'S-1208', tower:'Solstice',floor:'12th', bhk:'3 BHK', view:'Avenue', sqft:'2,580 sq.ft', price:'₹4.20 Cr', match:79 }, { no:'S-1509', tower:'Solstice',floor:'15th', bhk:'3 BHK', view:'Garden', sqft:'2,600 sq.ft', price:'₹4.44 Cr', match:75 }, ], journey:[ { label:'Walk-in registered', at:'12:11 PM', done:true, current:false }, { label:'Profile captured', at:'12:13 PM', done:true, current:false }, { label:'Awaiting sales', at:'12:13 PM', done:false, current:true }, { label:'Browsing units', at:'—', done:false, current:false }, { label:'Negotiation', at:'—', done:false, current:false }, { label:'Booking', at:'—', done:false, current:false }, ], }, { id:'WI-2050', name:'Vikram Patel', initials:'VP', stage:'browsing', config:'4 BHK · Penthouse', budget:'₹9–12 Cr', intent:'Investment', agent:'Self-guided', ago:'9 min ago', phone:'+91 98980 77451', email:'vikram@patelventures.in', matchScore:88, session:'Live · 9 min', prefs:{ typology:'4 BHK / Penthouse', sqft:'5,600–6,200 sq.ft', budget:'₹9–12 Cr', purpose:'Investment', timeline:'Opportunistic', family:'2 — no children', source:'Channel partner', mustHaves:['Penthouse / top 3 floors','Terrace deck','Two-car private garage','Riverfront'] }, matches:[ { no:'M-PH01', tower:'Meridian', floor:'30th (PH)', bhk:'5 BHK', view:'Riverfront', sqft:'6,120 sq.ft', price:'₹11.8 Cr', match:95 }, { no:'C-PH02', tower:'Celeste', floor:'29th (PH)', bhk:'5 BHK', view:'City + river', sqft:'5,980 sq.ft', price:'₹11.2 Cr', match:90 }, { no:'E-2810', tower:'Meridian', floor:'28th', bhk:'4 BHK', view:'Riverfront', sqft:'3,500 sq.ft', price:'₹9.10 Cr', match:82 }, { no:'A-PH03', tower:'Aurora', floor:'27th (PH)', bhk:'5 BHK', view:'Garden + skyline', sqft:'5,640 sq.ft', price:'₹9.95 Cr', match:78 }, ], journey:[ { label:'Walk-in registered', at:'11:53 AM', done:true, current:false }, { label:'Profile captured', at:'11:57 AM', done:true, current:false }, { label:'Browsing units', at:'12:01 PM', done:false, current:true }, { label:'With sales', at:'—', done:false, current:false }, { label:'Negotiation', at:'—', done:false, current:false }, { label:'Booking', at:'—', done:false, current:false }, ], }, { id:'WI-2051', name:'Sneha Reddy', initials:'SR', stage:'negotiating', config:'4 BHK · River view', budget:'₹6–6.8 Cr', intent:'End-use', agent:'Arjun S.', ago:'22 min ago', phone:'+91 90080 22914', email:'sneha.reddy@gmail.com', matchScore:91, session:'Live · 24 min', prefs:{ typology:'4 BHK', sqft:'3,400–3,520 sq.ft', budget:'₹6–6.8 Cr', purpose:'End-use', timeline:'Ready to close', family:'5 — joint family', source:'Google · search ad', mustHaves:['Riverfront view','Pooja room','Servant quarter','Floor 15–22'] }, matches:[ { no:'E-1807', tower:'Meridian', floor:'18th', bhk:'4 BHK', view:'Riverfront', sqft:'3,420 sq.ft', price:'₹6.52 Cr', match:97 }, { no:'E-1607', tower:'Meridian', floor:'16th', bhk:'4 BHK', view:'Riverfront', sqft:'3,420 sq.ft', price:'₹6.31 Cr', match:93 }, { no:'C-1904', tower:'Celeste', floor:'19th', bhk:'4 BHK', view:'River + city', sqft:'3,510 sq.ft', price:'₹6.74 Cr', match:88 }, { no:'C-1704', tower:'Celeste', floor:'17th', bhk:'4 BHK', view:'City', sqft:'3,360 sq.ft', price:'₹6.10 Cr', match:80 }, ], journey:[ { label:'Walk-in registered', at:'11:40 AM', done:true, current:false }, { label:'Profile captured', at:'11:44 AM', done:true, current:false }, { label:'Browsing units', at:'11:50 AM', done:true, current:false }, { label:'With sales — Arjun S.', at:'11:58 AM', done:true, current:false }, { label:'Negotiation', at:'12:04 PM', done:false, current:true }, { label:'Booking', at:'—', done:false, current:false }, ], }, { id:'WI-2052', name:'Karan Mehta', initials:'KM', stage:'followup', config:'3 BHK · Garden view', budget:'₹4.5–5 Cr', intent:'Upgrade', agent:'Meera K.', ago:'1 hr ago', phone:'+91 97370 55620', email:'karan.mehta@zoho.com', matchScore:76, session:'Offline · follow-up', prefs:{ typology:'3 BHK', sqft:'2,560–2,640 sq.ft', budget:'₹4.5–5 Cr', purpose:'Upgrade from 2 BHK', timeline:'Within a year', family:'4 — two children', source:'Past site visit', mustHaves:['Garden view','Club access','School within 2 km'] }, matches:[ { no:'A-1102', tower:'Aurora', floor:'11th', bhk:'3 BHK', view:'Podium garden', sqft:'2,610 sq.ft', price:'₹4.62 Cr', match:85 }, { no:'S-1305', tower:'Solstice',floor:'13th', bhk:'3 BHK', view:'Garden', sqft:'2,580 sq.ft', price:'₹4.48 Cr', match:81 }, { no:'A-0908', tower:'Aurora', floor:'9th', bhk:'3 BHK', view:'Avenue', sqft:'2,560 sq.ft', price:'₹4.30 Cr', match:74 }, { no:'S-1006', tower:'Solstice',floor:'10th', bhk:'3 BHK', view:'Garden + city', sqft:'2,600 sq.ft', price:'₹4.55 Cr', match:70 }, ], journey:[ { label:'Walk-in registered', at:'10:48 AM', done:true, current:false }, { label:'Profile captured', at:'10:52 AM', done:true, current:false }, { label:'Browsing units', at:'10:59 AM', done:true, current:false }, { label:'With sales — Meera K.', at:'11:10 AM', done:true, current:false }, { label:'Follow-up scheduled', at:'11:25 AM', done:false, current:true }, { label:'Booking', at:'—', done:false, current:false }, ], }, ], }; } // Resolve a stage key → its descriptor (label + dot colour). function sdStage(key) { const stages = (window.SALES_DESK && window.SALES_DESK.stages) || []; return stages.find(s => s.key === key) || { key, label: key, dot: 'var(--gold)' }; } function SdMiniIcon({ type }) { const c = { fill:'none', stroke:'currentColor', strokeWidth:1.7, strokeLinecap:'round', strokeLinejoin:'round' }; if (type === 'phone') return ; if (type === 'sms') return ; if (type === 'units') return ; if (type === 'search')return ; if (type === 'mail') return ; if (type === 'whatsapp') return ; if (type === 'user') return ; if (type === 'users') return ; if (type === 'clock') return ; if (type === 'link') return ; if (type === 'building') return ; if (type === 'ruler') return ; if (type === 'rupee') return ; if (type === 'calendar') return ; if (type === 'sparkle') return ; if (type === 'report') return ; if (type === 'arrowleft') return ; if (type === 'eye') return ; if (type === 'mute') return ; if (type === 'up') return ; if (type === 'compass') return ; if (type === 'frame') return ; if (type === 'star') return ; return null; } // map a free-text must-have to a fitting glyph, so the GRE chips read at a glance function mustHaveIcon(label) { const s = (label || '').toLowerCase(); if (/quiet|silent/.test(s)) return 'mute'; if (/park|garden|view|pool|skyline|deck/.test(s)) return 'eye'; if (/floor|high/.test(s)) return 'up'; if (/corner|terrace|penthouse|frame/.test(s)) return 'frame'; if (/vastu|compass|facing|direction/.test(s)) return 'compass'; if (/parking|car/.test(s)) return 'building'; return 'star'; } // ── Sales-Desk session helpers (module scope so the panel stays readable) ──── const sdCard = { background:'rgba(255,255,255,0.62)', border:'1px solid var(--line)', borderRadius:18, boxShadow:'0 18px 50px rgba(40,30,12,0.07), inset 0 1px 0 rgba(255,255,255,0.6)', backdropFilter:'blur(8px)', WebkitBackdropFilter:'blur(8px)', }; const sdEyebrow = { fontSize:14, letterSpacing:'0.34em', color:'var(--slate)' }; const sdInput = { padding:'14px 16px', borderRadius:12, border:'1px solid var(--line)', background:'rgba(255,255,255,0.82)', fontSize:17, color:'var(--ink)', outline:'none', fontFamily:'inherit' }; const DISP_TONE = { 'Interested':'#3a9d6a', 'Needs time':'#d99a2b', 'Evaluating options':'#5b8fb0', 'Not interested':'#b0564a' }; const PRIO_TONE = { Hot:'#d8472f', Warm:'#d99a2b', Cold:'#5b8fb0' }; function sdDur(ms){ const s=Math.max(0,Math.round(ms/1000)); const m=Math.floor(s/60); const r=s%60; return m? `${m}m ${r}s` : `${r}s`; } // LIVE session panel — replaces the console body once a journey is running. function SdActivePanel({ sess, record, onEnd }) { const snap = sess.snapshot(); const dur = sess.elapsed(); const stats = [ { v: sdDur(dur), k:'DURATION' }, { v: snap.screens.length, k:'SCREENS VISITED' }, { v: `${snap.modulesUsed}/${snap.modulesTotal}`, k:'MODULES USED' }, { v: snap.eventCount, k:'INTERACTIONS' }, ]; const maxMs = (snap.screens[0] && snap.screens[0].ms) || 1; return (
{/* live banner */}
{record ? record.initials : '—'}
JOURNEY LIVE
{record ? record.name : 'Walk-in'}
{sdDur(dur)}
ELAPSED ON FLOOR
{/* live stat cards */}
{stats.map(s => (
{s.v}
{s.k}
))}
{/* live screen breakdown */}
WHAT THEY ARE EXPLORING · LIVE
{snap.screens.length === 0 ? (
Waiting for the customer to start exploring…
) : (
{snap.screens.map(s => (
{s.label}
{sdDur(s.ms)}
×{s.visits}
))}
)}
{/* end journey */}
); } // END-OF-JOURNEY wrap-up form — captured before the session is saved. function SdWrapForm({ record, onCancel, onSubmit }) { const [disp, setDisp] = React.useState(null); const [prio, setPrio] = React.useState(null); const [date, setDate] = React.useState(''); const [time, setTime] = React.useState(''); const [remark, setRemark] = React.useState(''); const DISPOS = ['Interested','Needs time','Evaluating options','Not interested']; const ready = disp && prio; const lastName = record ? record.name.split(' ').slice(-1)[0] : 'this walk-in'; return (
END JOURNEY · CAPTURE OUTCOME
How did it go with {lastName}?
DISPOSITION
{DISPOS.map(d => { const on = disp===d; const c = DISP_TONE[d]; return ( ); })}
PRIORITY
{['Hot','Warm','Cold'].map(p => { const on = prio===p; const c = PRIO_TONE[p]; return ( ); })}
NEXT FOLLOW-UP
setDate(e.target.value)} style={{...sdInput, flex:'1 1 0'}}/> setTime(e.target.value)} style={{...sdInput, flex:'1 1 0'}}/>
REMARK