-
-
Excellence Guaranteed
-
Why Choose Us
-
Industry-leading standards combined with personalized service for an unmatched construction experience
-
-
-
-
-
-
-
- {WHY.map((w, i) => (
-
setWhy(i)}>
-
{String(i + 1).padStart(2, "0")}
-
-
{w.t}
- {i === why &&
{w.d}
}
-
+
+
+
+
+
Excellence Guaranteed
+
Why Choose Us
+
Industry-leading standards combined with personalized service for an unmatched construction experience
+
+
+
+
- ))}
+
+ {WHY.map((w, i) => (
+
setWhy(i)}>
+
+
+
+
+
{String(i + 1).padStart(2, "0")}
+
+
+
+ ))}
+
+
@@ -512,15 +592,15 @@ export default function Home() {
HOUSECOIN
-
-
- ☰ MENU
- PIQUE
- ✦ CONTACT
-
-
-
The PIQUE of Home Design
-
+
+
+
@@ -535,20 +615,21 @@ export default function Home() {
Every owner here is reachable for a reference call. Every plot is registered to verifiable YEIDA records.
- {TESTIMONIALS.map((t) => (
-
- “
-
- {t.quote}
-
-
-
- {t.name}
- {t.role}
-
-
-
- ))}
+ {/* LEFT */}
+
+
+
+
+ {/* CENTER */}
+
+
+
+
+ {/* RIGHT */}
+
+
+
+
View all Stories
diff --git a/src/app/privacy/page.tsx b/src/app/(site)/privacy/page.tsx
similarity index 100%
rename from src/app/privacy/page.tsx
rename to src/app/(site)/privacy/page.tsx
diff --git a/src/app/projects/page.tsx b/src/app/(site)/projects/page.tsx
similarity index 95%
rename from src/app/projects/page.tsx
rename to src/app/(site)/projects/page.tsx
index 2020583..c40c365 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/(site)/projects/page.tsx
@@ -26,10 +26,10 @@ const VILLAS = [
];
const ACROSS = [
- { n: "12", t: "Uttar Pradesh", icon: "🏛" },
- { n: "5", t: "Uttarakhand", icon: "⛰" },
- { n: "8", t: "Delhi NCR", icon: "🏙" },
- { n: "6", t: "Haryana", icon: "🌾" },
+ { n: "12", t: "Uttar Pradesh" },
+ { n: "5", t: "Uttarakhand" },
+ { n: "8", t: "Delhi NCR" },
+ { n: "6", t: "Haryana" },
];
const COMPANIES = [
@@ -43,10 +43,10 @@ export default function ProjectsPage() {
return (
<>
-
+
-
-
+
+
Our Projects
Showcasing the landmarks, infrastructure, and developments we have successfully delivered with quality, precision, and excellence.
@@ -129,12 +129,12 @@ export default function ProjectsPage() {
-
{v.bhk} {v.sqm}
-
{v.floor}
+
{v.bhk} {v.floor} {v.sqm}
+
-
{v.name}
-
{v.desc}
+
{v.name}
+ {v.desc}
FROM {v.price}
{v.months}
@@ -166,7 +166,9 @@ export default function ProjectsPage() {
Projects
{a.t}
-
{a.icon}
+
+
+
))}
diff --git a/src/app/services/page.tsx b/src/app/(site)/services/page.tsx
similarity index 100%
rename from src/app/services/page.tsx
rename to src/app/(site)/services/page.tsx
diff --git a/src/app/(site)/style.css b/src/app/(site)/style.css
new file mode 100644
index 0000000..44a7024
--- /dev/null
+++ b/src/app/(site)/style.css
@@ -0,0 +1,1630 @@
+@import url("https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Heebo:wght@300;400;500;600;700&family=Parkinsans:wght@300..800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Philosopher:wght@400;700&family=Gloock&display=swap");
+/* ------------------------------- tokens --------------------------------- */
+:root {
+--serif: "Libre Baskerville", "Baskerville Old Face", Georgia, serif;
+--body: "Heebo", system-ui, sans-serif;
+--ui: "Plus Jakarta Sans", "Heebo", sans-serif;
+--display: "Parkinsans", "Heebo", sans-serif;
+--ink: #151515;
+--ink-2: #636669;
+--ink-3: #a8acb4;
+--line: #e6e8ec;
+--bg: #ffffff;
+--bg-soft: #f5faff;
+--bg-blue: #e4f1ff;
+--green: #003616;
+--green-bright: #1d9e16;
+--navy: #0b1220;
+--navy-2: #0f1419;
+--blue: #0077ff;
+--blue-2: #2261ff;
+--amber: #ffc107;
+--orange: #f45700;
+--container: 1370px;
+--radius: 18px;
+--radius-lg: 28px;
+--shadow: 0 24px 60px rgba(13, 20, 33, 0.10);
+--shadow-sm: 0 12px 30px rgba(13, 20, 33, 0.08);
+}
+/* ------------------------------- reset ---------------------------------- */
+*,
+*::before,
+*::after { box-sizing: border-box; }
+* { margin: 0; padding: 0; }
+html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
+body {
+transition: all 0.3s ease 0s;
+font-family: var(--body);
+color: var(--ink);
+background: var(--bg);
+line-height: 1.6;
+-webkit-font-smoothing: antialiased;
+text-rendering: optimizeLegibility;
+overflow-x: clip;
+}
+img { max-width: 100%; display: block; }
+a { color: inherit; text-decoration: none; }
+button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
+ul { list-style: none; }
+h1, h2, h3, h4, h5 { font-weight: 400; line-height: 1.12; }
+/* ----------------------------- helpers ---------------------------------- */
+.container {
+width: 100%;
+max-width: var(--container);
+margin: 0 auto;
+}
+.section { padding: 90px 0; }
+.eyebrow {
+font-family: var(--body);
+font-size: 12px;
+letter-spacing: 0.18em;
+text-transform: uppercase;
+color: var(--blue);
+font-weight: 500;
+margin-bottom: 18px;
+}
+.h-serif { font-family: var(--serif); }
+.section-title {
+font-family: var(--serif);
+font-size: 50px;
+line-height: 1.08;
+color: var(--ink);
+letter-spacing: -0.01em;
+}
+.section-head {
+display: flex;
+justify-content: space-between;
+align-items: flex-end;
+gap: 40px;
+margin-bottom: 48px;
+}
+.section-head .lead { max-width: 420px; color: var(--ink-2); font-size: 16px; }
+/* buttons */
+.btn {
+display: inline-flex;
+align-items: center;
+gap: 10px;
+font-family: var(--ui);
+font-weight: 500;
+font-size: 15px;
+padding: 14px 26px;
+border-radius: 100px;
+transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
+}
+.btn:hover { transform: translateY(-2px); }
+.btn-dark { background: var(--navy); color: #fff; }
+.btn-light { background: #fff; color: var(--ink); box-shadow: var(--shadow-sm); }
+.btn-ghost { border: 1px solid rgba(255, 255, 255, 0.5); color: #fff; }
+.btn .arrow { font-size: 16px; }
+/* ============================== HEADER ================================== */
+.site-header {
+position: absolute;
+top: 40px;
+left: 0;
+right: 0;
+z-index: 50;
+padding: 28px 0;
+}
+.header-inner {
+max-width: 1835px;
+margin: 0 auto;
+padding: 0 50px;
+display: flex;
+align-items: center;
+justify-content: space-between;
+gap: 24px;
+}
+.brand { display: flex; align-items: center; gap: 12px; color: #fff; }
+.brand-emblem {
+width: 54px; height: 54px; border-radius: 50%;
+display: grid; place-items: center;
+background: rgba(255, 255, 255, 0.12);
+overflow: hidden;
+}
+.brand-emblem img { width: 100%; height: 100%; object-fit: cover; }
+.brand-text {
+font-family: var(--display);
+font-size: 15px;
+font-weight: 700;
+line-height: 1.05;
+letter-spacing: 0.06em;
+text-transform: uppercase;
+}
+.nav-pill {
+display: flex;
+align-items: center;
+gap: 6px;
+padding: 8px;
+border-radius: 100px;
+background: rgba(255, 255, 255, 0.71);
+border: 1px solid #FFFFFF;
+backdrop-filter: blur(100px);
+border-radius: 100px;
+}
+.nav-link {
+font-family: var(--display);
+font-size: 15px;
+color: #1d2430;
+padding: 11px 20px;
+border-radius: 100px;
+white-space: nowrap;
+transition: background 0.2s ease, color 0.2s ease;
+}
+.nav-link:hover { background: #eef2f7; }
+.nav-link.active { background: white; color: #000000; }
+.header-right { display: flex; align-items: center; gap: 14px; }
+.social-dots { display: flex; gap: 8px; }
+.social-dots .dot {
+width: 38px; height: 38px; border-radius: 50%;
+display: grid; place-items: center;
+background: rgba(255, 255, 255, 0.16);
+color: #fff; font-size: 13px;
+border: 1px solid rgba(255, 255, 255, 0.25);
+transition: background 0.2s ease;
+}
+.social-dots .dot:hover { background: rgba(255, 255, 255, 0.32); }
+.social-dots .dot img { width: 18px; height: 18px; object-fit: contain; }
+.btn-login {
+font-family: var(--display);
+background: #fff; color: var(--ink);
+padding: 11px 26px; border-radius: 100px; font-size: 15px;
+transition: transform 0.2s ease;
+}
+.btn-login:hover { transform: translateY(-2px); }
+.menu-btn {
+width: 46px; height: 46px; border-radius: 14px;
+background: #fff; display: none;
+flex-direction: column; gap: 4px; align-items: center; justify-content: center;
+}
+.menu-btn span { width: 18px; height: 2px; background: var(--ink); display: block; }
+/* ============================== FOOTER ================================== */
+.site-footer {
+position: relative;
+margin-top: 0;
+background: #050c1b;
+color: #d7dce5;
+overflow: hidden;
+padding-top: 280px;
+}
+.site-footer::before {
+content: "";
+position: absolute;
+inset: 0;
+background:
+linear-gradient(180deg, rgba(5, 12, 27, 0.35) 0%, rgba(5, 12, 27, 0.9) 70%, #050c1b 100%),
+url("/img/footer.png") center top / cover no-repeat;
+opacity: 0.9;
+}
+.testimonials {
+background: #F5FAFF;
+}
+.footer-watermark {
+position: absolute;
+top: 70px;
+left: 50%;
+transform: translateX(-50%);
+font-family: var(--display);
+font-weight: 700;
+font-size: 220px;
+line-height: 1;
+color: rgba(255, 255, 255, 0.08);
+white-space: nowrap;
+pointer-events: none;
+}
+.footer-inner { position: relative; z-index: 2; padding-bottom: 0; }
+.footer-grid {
+display: grid;
+grid-template-columns: 1.6fr 1fr 1fr 1.3fr;
+gap: 40px;
+border-top: 1px solid rgba(255, 255, 255, 0.08);
+}
+.footer-brand { max-width: 440px;
+border-left: 1px solid #ffffff1a;
+padding: 40px 0 40px 30px; }
+.footer-emblem {
+}
+.footer-content {
+z-index: 5;
+border-top: 1px solid #ffffff1a;
+border-bottom: 1px solid #ffffff1a;
+width: 100%;
+padding-bottom: 0;
+position: relative;
+}
+.footer-emblem img { margin-bottom: 20px;}
+.footer-brand p { font-size: 15px; color: white; line-height: 1.7; }
+.footer-reg { margin-top: 20px; font-size: 13px; color: #7e8798; font-family: var(--ui); }
+.footer-col h4 {
+font-family: var(--ui);
+font-weight: 600;
+font-size: 17px;
+color: #fff;
+margin-bottom: 22px;
+letter-spacing: 0.02em;
+}
+.footer-col a, .footer-col p {
+display: block;
+font-family: var(--ui);
+font-size: 15px;
+color: white;
+margin-bottom: 14px;
+transition: color 0.2s ease;
+}
+.footer-col a:hover { color: #fff; }
+.footer-bottom {
+position: relative;
+z-index: 2;
+display: flex;
+justify-content: space-between;
+align-items: center;
+gap: 20px;
+padding: 20px 0;
+border-top: 1px solid rgba(255, 255, 255, 0.08);
+}
+.footer-bottom p { font-family: var(--ui); font-size: 14px; color: #8a93a3; }
+.footer-social { display: flex; gap: 12px; }
+.footer-social a {
+width: 34px; height: 34px; border-radius: 50%;
+display: grid; place-items: center;
+background: rgba(255, 255, 255, 0.08);
+font-size: 13px; color: #cfd6e2;
+transition: background 0.2s ease;
+}
+.footer-social a:hover { background: rgba(255, 255, 255, 0.2); }
+.footer-social a img { width: 16px; height: 16px; object-fit: contain; }
+/* ----------------------- generic inner page hero ------------------------ */
+.page-hero {
+position: relative;
+padding: 220px 0 90px;
+color: #fff;
+text-align: center;
+background: linear-gradient(180deg, rgba(5, 12, 27, 0.55), rgba(5, 12, 27, 0.75));
+}
+.page-hero h1 { font-family: var(--serif); font-size: 64px; margin-bottom: 18px; }
+.page-hero p { max-width: 620px; margin: 0 auto; color: rgba(255, 255, 255, 0.85); }
+.page-hero .crumbs { font-family: var(--ui); font-size: 14px; letter-spacing: 0.05em; margin-bottom: 22px; color: rgba(255,255,255,0.8); }
+/* ===================== carousels (drag / swipe) ========================= */
+.plots-rail, .ab-team-grid { cursor: grab; }
+.is-dragging { cursor: grabbing; user-select: none; scroll-behavior: auto !important; }
+.is-dragging * { pointer-events: none; }
+/* ============================ RESPONSIVE ================================ */
+@media (max-width: 1200px) {
+.section-title { font-size: 40px; }
+.footer-grid { grid-template-columns: 1fr 1fr; }
+.footer-brand { grid-column: 1 / -1; max-width: none; }
+}
+@media (max-width: 1024px) {
+.nav-pill {
+position: absolute;
+top: 88px; right: 50px;
+flex-direction: column;
+align-items: stretch;
+display: none;
+}
+.nav-pill.is-open { display: flex; }
+.menu-btn { display: flex; }
+.social-dots { display: none; }
+}
+@media (max-width: 768px) {
+.container { padding: 0 20px; }
+.header-inner { padding: 0 20px; }
+.section { padding: 60px 0; }
+.section-title { font-size: 32px; }
+.section-head { flex-direction: column; align-items: flex-start; gap: 16px; }
+.footer-grid { grid-template-columns: 1fr; }
+.footer-watermark { font-size: 90px; top: 90px; }
+.footer-bottom { flex-direction: column; align-items: flex-start; }
+.page-hero h1 { font-size: 40px; }
+}
+/* ==========================================================================
+HOME PAGE
+========================================================================== */
+/* ------------------------------ hero ------------------------------------ */
+.hero {
+position: relative;
+height: 900px;
+margin: 30px 30px;
+border-radius: 30px;
+overflow: hidden;
+display: flex;
+align-items: center;
+}
+.hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.hero-overlay {
+position: absolute;
+inset: 0;
+background: linear-gradient(258.88deg, rgba(0, 0, 0, 0) 22.04%, #0077FF 104.42%);
+border-radius: 30px 0px 0px 30px;
+}
+.hero-content {
+z-index: 2;
+color: #fff;
+max-width: 820px;
+padding: 0 90px;
+position: relative;
+margin-top: 290px;
+}
+.hero-content h1 {
+font-family: var(--serif);
+font-size: 50px;
+line-height: 1.15;
+margin-bottom: 22px;
+}
+.hero-content h1 span { display: inline-block;
+font-weight: 600;}
+.hero-sub {color: #ffffffe0;
+margin-bottom: 34px;
+font-size: 18px;
+font-weight: 400; }
+.hero-cta { font-size: 15px; }
+.hero-note {
+position: absolute;
+right: 60px;
+bottom: 60px;
+z-index: 2;
+text-align: right;
+color: rgba(255, 255, 255, 0.9);
+font-size: 18px;
+font-weight: 500;
+line-height: 1.4;
+}
+/* --------------------------- feature strip ------------------------------ */
+.feature-strip {
+border: 1px solid var(--line);
+background: #fff;
+margin-top: 4px;
+padding: 26px 0;
+overflow: hidden;
+margin: 0 30px;
+border-radius: 11px;
+}
+.feature-track {
+display: flex;
+gap: 60px;
+width: max-content;
+animation: marquee 32s linear infinite;
+white-space: nowrap;
+}
+.feature-item {
+font-family: var(--ui);
+color: var(--ink);
+align-items: center;
+gap: 14px;
+font-size: 16px;
+font-weight: 500;
+display: inline-flex;
+text-transform: uppercase;
+}
+.dot-sq { width: 7px; height: 7px; background: var(--ink-3); display: inline-block; transform: rotate(45deg); }
+@keyframes marquee {
+from { transform: translateX(0); }
+to { transform: translateX(-50%); }
+}
+/* ------------------------------ plots ----------------------------------- */
+.head-aside { align-items: flex-end;
+gap: 92px;
+display: flex; }
+.plots-rail {
+display: flex;
+gap: 24px;
+overflow-x: auto;
+ padding-bottom: 54px;
+scroll-snap-type: x mandatory;
+scrollbar-width: none;
+}
+.plots-rail::-webkit-scrollbar { display: none; }
+.villa-card {
+scroll-snap-align: start;
+flex: 0 0 calc((100% - 48px) / 3);
+transition: box-shadow .25s, transform .25s;
+overflow: hidden;
+position: relative;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+border-radius: 20px;
+}
+.walkthrough .lead {
+text-align: right;
+}
+.villa-card:hover { box-shadow: var(--shadow); transform: translateY(-4px); }
+.villa-media {
+border-radius: 16px;
+height: 260px;
+padding: 10px;
+position: relative; }
+.villa-media img { object-fit: cover;
+width: 100%;
+height: 100%;
+border-radius: 12px; }
+.villa-tags { gap: 8px;
+display: flex;
+position: absolute;
+bottom: 21px;
+left: 22px; }
+.villa-tags span {
+color: #000000;
+-webkit-backdrop-filter: blur(4px);
+backdrop-filter: blur(4px);
+background: #ffffff;
+border-radius: 38px;
+padding: 5px 10px;
+font-size: 11px;
+}
+.villa-floor {
+right: 12px; bottom: 12px;
+width: 30px; height: 30px; border-radius: 8px;
+background: rgba(10, 16, 28, 0.78); color: #fff;
+display: grid; place-items: center; font-size: 12px;
+}
+.villa-body {align-items: self-start;
+display: flex;
+padding: 10px 20px 0;
+justify-content: space-between;}
+.villa-body h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 8px; }
+.villa-desc { font-size: 13px; color: var(--ink-2); margin-bottom: 18px; }
+.villa-foot { display: flex; justify-content: space-between; align-items: flex-end; padding-top: 16px; }
+.villa-price { font-family: var(--serif);
+flex-direction: column;
+font-size: 18px;
+display: flex;
+text-align: end; }
+.villa-price small { font-family: var(--body); font-size: 11px; color: var(--ink-3); letter-spacing: 0.1em; }
+.villa-months { color: var(--ink-2);
+font-size: 12px;
+position: absolute;
+top: 20px;
+color: #fff;
+-webkit-backdrop-filter: blur(4px);
+backdrop-filter: blur(4px);
+background: #0a101cc7;
+border-radius: 38px;
+padding: 6px 16px;
+font-size: 11px;
+right: 20px; }
+.plots-pager { display: flex; align-items: center; gap: 24px; margin-top: 36px; }
+.pager-count { font-size: 14px; color: var(--ink); font-weight: 600; }
+.pager-line { flex: 1; height: 2px; background: var(--line); position: relative; }
+.pager-line span { position: absolute; left: 0; top: 0; height: 100%; width: 18%; background: var(--blue); }
+.pager-btns { display: flex; gap: 10px; }
+.pager-btns button { width: 42px; height: 42px; border: 1px solid var(--line); border-radius: 50%; color: var(--ink-2); transition: background 0.2s; }
+.pager-btns button:hover { background: #f2f5f9; }
+.pager-btns button img { width: 16px; height: 16px; display: block; margin: auto; }
+.pager-btns button.pager-prev img { transform: rotate(180deg); }
+/* --------------------------- walkthrough -------------------------------- */
+.walk-stage {
+height: 620px;
+position: relative;
+overflow: hidden;
+border-radius: 20px 20px 0 0;
+}
+.walk-stage {
+position: relative;
+}
+.corner {
+position: absolute;
+width: 65px;
+height: 65px;
+z-index: 5;
+}
+/* Top Left */
+.corner.tl {
+top: 30px;
+left: 30px;
+border-top: 1px solid #fff;
+border-left: 1px solid #fff;
+}
+/* Top Right */
+.corner.tr {
+top: 30px;
+right: 30px;
+border-top: 1px solid #fff;
+border-right: 1px solid #fff;
+}
+/* Bottom Left */
+.corner.bl {
+bottom: 30px;
+left: 30px;
+border-bottom: 1px solid #fff;
+border-left: 1px solid #fff;
+}
+/* Bottom Right */
+.corner.br {
+bottom: 30px;
+right: 30px;
+border-bottom: 1px solid #fff;
+border-right: 1px solid #fff;
+}
+.walk-stage img { width: 100%; height: 100%; object-fit: cover; }
+.walk-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.55)); }
+.play-btn {
+position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
+width: 84px; height: 84px; border-radius: 50%;
+background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(6px);
+border: 1px solid rgba(255, 255, 255, 0.5);
+display: grid; place-items: center; color: #fff; font-size: 26px;
+transition: transform 0.2s, background 0.2s;
+}
+.play-btn:hover { transform: translate(-50%, -50%) scale(1.08); background: rgba(255, 255, 255, 0.4); }
+.walk-caption { color: #fff;
+font-family: var(--serif);
+font-size: 23px;
+position: absolute;
+bottom: 48px;
+left: 66px; }
+.walk-meta { color: #fffc;
+font-size: 14px;
+position: absolute;
+bottom: 50px;
+right: 56px; }
+.walk-tabs {
+box-shadow: var(--shadow-sm);
+background: #fff;
+border-radius: 0px;
+grid-template-columns: repeat(4, 1fr);
+gap: 0;
+margin-top: 0px;
+display: grid;
+overflow: hidden; }
+.walk-tab { padding: 22px; font-weight: 500; font-size: 16px; color: var(--ink-2); transition: background 0.2s, color 0.2s; border-right: 1px solid var(--line); }
+.walk-tab:last-child { border-right: none; }
+.walk-tab.active { background: var(--blue-2); color: #fff; font-weight: 600; }
+/* ----------------- dream realized (scroll-driven stages) ---------------- */
+/* Tall track: the inner pins to the viewport while you scroll through it,
+and the active stage (0–3) re-treats the same villa view. */
+.dream { position: relative; height: 440vh; }
+.dream-sticky {
+position: sticky; top: 0; height: 95vh;
+overflow: hidden; display: flex; flex-direction: column;
+}
+/* one image that morphs across every stage */
+.dream-bg {
+position: absolute; inset: 0; z-index: 0;
+width: 100%; height: 100%; object-fit: cover;
+transition: filter 1s ease, transform 1.4s ease;
+will-change: filter, transform;
+}
+.dream[data-stage="0"] .dream-bg { transform: scale(1.12);
+background: linear-gradient(180deg, rgba(0, 0, 0, 0) 3.21%, rgba(0, 0, 0, 0.34) 72.64%, rgba(0, 0, 0, 0.5) 100%); }
+.dream[data-stage="1"] .dream-bg { filter: brightness(0.5) sepia(1) hue-rotate(168deg) saturate(6) contrast(1.2); transform: scale(1.08); }
+.dream[data-stage="2"] .dream-bg { filter: sepia(0.5) saturate(1.15) brightness(0.9) contrast(1.05); transform: scale(1.04); }
+.dream[data-stage="3"] .dream-bg { filter: none; transform: scale(1); }
+/* blueprint grid — fades in only for the blueprint stage */
+.dream-grid {
+position: absolute; inset: 0; z-index: 1; pointer-events: none;
+opacity: 0; transition: opacity 0.9s ease; mix-blend-mode: screen;
+background-image:
+linear-gradient(rgba(120, 180, 255, 0.4) 1px, transparent 1px),
+linear-gradient(90deg, rgba(120, 180, 255, 0.4) 1px, transparent 1px);
+background-size: 46px 46px;
+}
+.dream[data-stage="1"] .dream-grid { opacity: 1; }
+/* construction scanlines — fade in only for the construction stage */
+.dream-scan {
+position: absolute; inset: 0; z-index: 1; pointer-events: none;
+opacity: 0; transition: opacity 0.9s ease;
+background: repeating-linear-gradient(0deg, rgba(255, 196, 84, 0.07) 0 3px, transparent 3px 7px);
+}
+.dream[data-stage="2"] .dream-scan { opacity: 1; }
+.dream-overlay {
+position: absolute; inset: 0; z-index: 2; pointer-events: none;
+background: linear-gradient(180deg, rgba(6, 18, 40, 0.5), rgba(6, 18, 40, 0.12) 38%, rgba(6, 18, 40, 0.7));
+}
+/* copy block — vertically centred in the space above the cards */
+.dream-inner::before {
+content: "";
+border: 1px solid #fff;
+border-image: initial;
+width: 92%;
+height: 81%;
+position: absolute;
+top: 50%;
+left: 50%;
+transform: translate(-50%, -50%);
+}
+.dream-inner {
+z-index: 3;
+flex: 1;
+display: flex;
+position: relative;
+}
+.dream-inner-in { align-items: flex-start;
+gap: 28px;
+padding: 0 24px;
+margin-top: 120px;
+display: flex;
+min-width: 86%; }
+.dream-rail { display: flex; flex-direction: column; gap: 14px; padding-top: 8px; }
+.dream-dot {
+width: 14px; height: 14px; border-radius: 50%;
+border: 1.5px solid rgba(255, 255, 255, 0.45); position: relative; transition: all 0.4s ease;
+}
+.dream-dot > span {
+position: absolute; inset: 3px; border-radius: 50%; background: #fff;
+transform: scale(0); transition: transform 0.4s ease;
+}
+.dream-dot.done { border-color: rgba(255, 255, 255, 0.75); }
+.dream-dot.done > span { transform: scale(1); opacity: 0.6; }
+.dream-dot.on { border-color: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.14); }
+.dream-dot.on > span { transform: scale(1); }
+.dream-copy { color: #fff; max-width: 560px; animation: dreamFade 0.7s ease; }
+@keyframes dreamFade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }
+.dream-stage { font-size: 13px; letter-spacing: 0.22em; margin-bottom: 14px; opacity: 0.85; }
+.dream-title { font-family: var(--serif); margin-bottom: 16px; font-size: 54px; line-height: 1.05; }
+.dream-sub { font-size: 16px; color: rgba(255, 255, 255, 0.88); line-height: 1.6; }
+/* bottom progress strip */
+.stage-cards.active h4 {
+color: white;
+}
+.dream-rail {
+display: none;
+}
+.stage-cards {
+z-index: 3;
+background: #ffffff1f;
+grid-template-columns: repeat(4, 1fr);
+gap: 1px;
+display: grid;
+position: relative;
+top: -3px;
+}
+.stage-card {
+color: #000000;
+background: #ffffff;
+padding: 24px 26px;
+transition: background .5s, color .5s;
+}
+.stage-num { font-size: 12px; letter-spacing: 0.2em; opacity: 0.7; }
+.stage-card h4 { font-family: var(--serif); font-size: 19px; margin: 8px 0; color: black; }
+.stage-card p { font-size: 12.5px; line-height: 1.5; color: black; }
+.stage-card.past { opacity: 0.9; }
+.stage-card.active { background: var(--blue); color: #fff; }
+.stage-card.active h4 { color: #fff; }
+.stage-card.active p { color: rgba(255, 255, 255, 0.92); }
+.stage-card.active .stage-num { opacity: 0.95; }
+/* ---------------------------- landmark ---------------------------------- */
+.landmark {
+background: #F5FAFF;
+}
+.proj-body {
+padding-top: 20px;
+}
+.proj-tabs {
+ background: #ffffff;
+ border-radius: 10px;
+ gap: 8px;
+ padding: 6px;
+ display: inline-flex;
+ }
+.proj-tabs button { padding: 12px 24px; border-radius: 8px; font-size: 15px; color: var(--ink-2); font-weight: 500; }
+.proj-tabs button.active { background: var(--blue-2); color: #fff; }
+.proj-big { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px; }
+.proj-small { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
+.proj-card { border: 1px solid var(--line);
+background: #fff;
+border-radius: 20px;
+overflow: hidden;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px;
+padding: 25px; }
+.proj-card:hover { box-shadow: var(--shadow-sm); }
+.proj-media { position: relative; }
+.proj-card.big .proj-media { height: 320px;
+border-radius: 20px; }
+.proj-card.small .proj-media { height: 220px; }
+.proj-media img { width: 100%; height: 100%; object-fit: cover; border-radius: 20px; }
+.proj-badge { position: absolute; top: 16px; right: 16px; background: var(--green-bright); color: #fff; font-size: 13px; font-weight: 600; padding: 7px 16px; border-radius: 100px; }
+.proj-badge.dark { background: var(--navy); }
+.proj-body h3 { font-family: var(--serif); font-size: 26px; margin-bottom: 12px; }
+.proj-card.small .proj-body h3 { font-size: 22px; }
+.proj-body p {
+color: var(--ink-2);
+margin-bottom: 22px;
+font-size: 16px;
+width: 80%;
+}
+.proj-stats {
+border-top: 1px solid var(--line);
+gap: 28px;
+padding-top: 18px;
+display: flex;
+justify-content: space-between;
+}
+.proj-stats div { display: flex; flex-direction: column; }
+.proj-stats strong { font-family: var(--serif); font-weight: 400; font-size: 22px; }
+.proj-card.small .proj-stats strong { font-size: 20px; }
+.proj-stats span { font-size: 13px; color: var(--ink-2); }
+/* ----------------------------- journey ---------------------------------- */
+.journey-list { display: flex; flex-direction: column; gap: 16px; }
+.journey-card {
+display: grid;
+grid-template-columns: 300px 1fr;
+gap: 30px;
+align-items: stretch;
+padding: 22px;
+cursor: pointer;
+position: relative;
+transition: box-shadow 0.2s, border-color 0.2s;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px;
+}
+.journey-card.active { box-shadow: var(--shadow-sm); border-color: #d3dae6; }
+.journey-image {
+border-radius: 14px;
+width: 100%;
+height: 380px;
+overflow: hidden;}
+.journey-image img { width: 100%; height: 100%; object-fit: cover; }
+.journey-content { display: flex; flex-direction: column; }
+.journey-top {
+display: flex;
+justify-content: space-between;
+align-items: flex-start;
+border-bottom: 1px solid #8080802e;
+padding-bottom: 20px;
+margin-bottom: 8px; }
+.journey-week { color: var(--blue);
+letter-spacing: 3px;
+font-size: 13px;
+font-weight: 600;
+text-transform: uppercase; }
+.journey-top h3 { font-family: var(--serif);
+margin-top: 8px;
+font-size: 26px; }
+.journey-step { display: flex; flex-direction: column; align-items: flex-end; }
+.journey-step span { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--ink-3); }
+.journey-step strong { color: #000000;
+font-size: 22px;
+font-weight: 500; }
+.virtual {
+background: #F5FAFF;
+}
+.journey-middle { display: grid; grid-template-columns: 1.4fr 1fr; gap: 30px; flex: 1; }
+.journey-sub { font-family: var(--serif);
+color: var(--ink);
+font-size: 21px;
+line-height: 1.4;
+width: 87%;
+margin-top: 50px; }
+.journey-body { color: var(--ink-2);
+margin-top: 32px;
+font-size: 16px;
+width: 80%;
+padding-bottom: 20px; }
+.journey-right-content { display: flex; flex-direction: column; gap: 10px; }
+.journey-item {
+color: #000000;
+border-bottom: 1px dashed var(--line);
+justify-content: space-between;
+padding-bottom: 8px;
+font-size: 16px;
+display: flex;
+}
+.journey-item .day { color: #070707; }
+.journey-thumb { position: absolute;
+bottom: 32px;
+right: 22px; }
+/* ---- pinned scroll reveal (Your Journey) ---- */
+.journey-pin.is-pinned { position: relative; }
+.journey-thumb img {
+height: 90px;
+width: 110px;
+border-radius: 10px;
+object-fit: cover;
+}
+.journey-pin.is-pinned .journey-sticky {
+position: sticky;
+top: 0;
+flex-direction: column;
+justify-content: center; }
+.journey-pin.is-pinned .journey-list { position: relative; display: block; }
+.journey-pin.is-pinned .journey-card {
+position: absolute;
+left: 0;
+right: 0;
+top: 0;
+z-index: 1;
+opacity: 0;
+visibility: hidden;
+transform: translateY(34px) scale(0.985);
+transition: opacity 0.6s ease, transform 0.6s ease;
+pointer-events: none; }
+.journey-pin.is-pinned .journey-card.active {
+position: relative;
+z-index: 3;
+opacity: 1;
+visibility: visible;
+transform: none;
+pointer-events: auto; }
+/* next card peeks out below the active one, like a stacked deck */
+.journey-pin.is-pinned .journey-card.next {
+z-index: 2;
+opacity: 0.6;
+visibility: visible;
+transform: translateY(170px) scale(0.96); }
+.journey-pin.is-pinned .journey-item {
+opacity: 0;
+transform: translateX(20px);
+transition: opacity 0.5s ease, transform 0.5s ease; }
+.journey-pin.is-pinned .journey-item.show { opacity: 1; transform: none; }
+/* ----------------------------- virtual ---------------------------------- */
+.virtual-head { text-align: center; max-width: 640px; margin: 0 auto 44px; }
+.virtual-head .lead { color: var(--ink-2); margin-top: 16px; }
+.virtual-stage {
+max-width: 1100px;
+height: 560px;
+margin: 0 auto;
+position: relative;
+overflow: hidden;
+background: white;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px;
+padding: 20px;
+}
+.virtual-stage img { object-fit: cover;
+width: 100%;
+height: 463px;
+border-radius: 20px; }
+.virtual-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.5)); }
+.virtual-caption { color: #fff;
+position: absolute;
+bottom: 110px;
+left: 60px; }
+.virtual-caption h3 { font-size: 28px; font-weight: 500; margin-bottom: 6px; }
+.virtual-caption p { font-size: 15px; color: rgba(255, 255, 255, 0.85); }
+.virtual-stage img::before {
+content: "";
+position: absolute;
+width: 100%;
+height: 100%;
+top: 0;
+/* Rectangle 34624406 */
+position: absolute;
+width: 1000px;
+height: 269.49px;
+left: calc(50% - 1000px/2 - 0.24px);
+top: 7190.96px;
+background: linear-gradient(200.28deg, rgba(0, 0, 0, 0) 29.2%, rgba(0, 7, 0, 0.62) 59.62%, #000F00 96.52%);
+mix-blend-mode: plus-darker;
+opacity: 0.84;
+border-radius: 0px 0px 15px 15px;
+}
+.virtual-features {
+position: absolute; left: 0; right: 0; bottom: 0;
+display: flex; justify-content: center; gap: 34px; flex-wrap: wrap;
+background: rgb(255, 255, 255); padding: 25px;
+}
+.virtual-features span { font-size: 14px; color: var(--ink); display: inline-flex; align-items: center; gap: 8px; }
+.virtual-features span::first-letter { color: var(--green-bright); }
+.virtual-features img {
+width: 15px;
+height: 15px;
+}
+/* ---------------------------- companies --------------------------------- */
+.companies-head { margin-bottom: 50px; }
+.companies-head .lead.center { margin: 16px 0 0; max-width: 560px; color: var(--ink-2); }
+.companies-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
+.company-card { padding: 32px;
+transition: box-shadow .2s, transform .2s;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px; }
+.company-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-4px); }
+.company-logo { height: 60px; display: flex; align-items: center; margin-bottom: 24px; }
+.company-logo img { max-height: 56px; width: auto; }
+.company-mark { font-family: var(--display); font-weight: 700; color: var(--orange); font-size: 20px; }
+.company-country { font-size: 13px; color: var(--ink-3); }
+.company-card h3 { font-family: var(--serif); font-size: 26px; margin: 6px 0 16px; }
+.company-tag { font-size: 17px; color: var(--ink); margin-bottom: 12px; }
+.company-desc { font-size: 14px; color: var(--ink-2); margin-bottom: 26px; }
+.company-stats { display: flex; gap: 40px; padding-top: 22px; border-top: 1px solid var(--line); }
+.company-stats strong { font-family: var(--serif); font-weight: 400; font-size: 20px; display: block; }
+.company-stats span { font-size: 13px; color: var(--ink-2); }
+/* ---------------------------- why choose -------------------------------- */
+.why-head { max-width: 560px; margin-bottom: 40px; }
+.why-head .lead { margin-top: 16px; color: var(--ink-2); }
+.why-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 40px; align-items: start; }
+.why-visual { overflow: hidden; height: 620px; display: flex;
+align-items: end;}
+.ab-experts {
+ background: #F5FAFF;
+}
+.why-tall { width: 380px;
+height: 510px;
+object-fit: unset;
+border-radius: 20px; }
+.why-list { position: relative; }
+.why-row { display: flex; flex-direction: column; padding: 22px 4px; border-bottom: 1px solid var(--line); cursor: pointer; }
+.why-row-head { display: flex; gap: 20px; align-items: flex-start; }
+.why-media { max-height: 0; opacity: 0; overflow: hidden; border-radius: 18px;
+transition: max-height 0.6s ease, opacity 0.5s ease, margin-bottom 0.6s ease; }
+.why-row.open .why-media { max-height: 280px; opacity: 1; margin-bottom: 18px; }
+.why-wide { width: 100%; height: 240px; object-fit: cover; display: block; }
+.why-no { font-family: var(--display); font-size: 16px; font-weight: 500; color: var(--ink-3); transition: color 0.3s ease; }
+.why-text h4 { font-family: var(--serif); font-size: 22px; }
+.why-desc { font-size: 15px; color: var(--ink-2);
+max-height: 0; opacity: 0; overflow: hidden;
+transition: max-height 0.5s ease, opacity 0.5s ease, margin-top 0.5s ease; }
+.why-row.open .why-desc { max-height: 120px; opacity: 1; margin-top: 8px; }
+.why-row.open .why-text h4 { color: var(--blue); }
+.why-row.open .why-no { color: var(--blue); }
+/* ---- pinned scroll reveal (Why Choose Us, right side) ---- */
+.why-pin.is-pinned { position: relative; }
+.why-pin.is-pinned .why-sticky {
+position: sticky; top: 0; min-height: 100vh;
+display: flex; flex-direction: column; justify-content: center; }
+/* --------------------------- showcase ----------------------------------- */
+.showcase { position: relative; padding: 90px 0; overflow: hidden; }
+.showcase-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.showcase-overlay { position: absolute; inset: 0; background: rgba(6, 16, 35, 0.45); }
+.showcase-brand { position: absolute; left: 50px; bottom: 40px; z-index: 3; color: #fff; font-family: var(--display); font-weight: 700; font-size: 26px; letter-spacing: 0.04em; }
+.showcase-card {
+z-index: 2;
+width: min(910px, 86%);
+margin: 0 auto;
+position: relative;
+overflow: hidden;
+box-shadow: 0 40px 80px #0006;
+background: #FFFFFF;
+border-radius: 30px;
+padding: 20px;
+}
+.showcase-card > img, .showcase-card > video { width: 100%; height: 520px; object-fit: cover; display: block; border-radius: 20px; }
+.showcase-bar { position: absolute; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 22px 28px; color: #fff; }
+.sc-menu, .sc-contact { font-size: 13px; letter-spacing: 0.08em; }
+.sc-logo { font-family: var(--display); font-weight: 700; font-size: 22px; letter-spacing: 0.1em; }
+.showcase-caption { position: absolute; left: 32px; bottom: 32px; color: #fff; }
+.showcase-caption h3 { font-family: var(--serif); font-size: 34px; line-height: 1.1; }
+/* -------------------------- testimonials -------------------------------- */
+.testi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: start; }
+.testi-col { display: flex; flex-direction: column; gap: 10px; }
+.testi-card { background: #fff; border: 1px solid var(--line); border-radius: 20px; padding: 30px;
+box-shadow: 0 10px 45px rgba(0, 0, 0, 0.04); }
+.testi-card.large { flex: 1; }
+.quote-mark {
+font-family: var(--serif);
+color: var(--ink-3);
+height: 26px;
+font-size: 50px;
+line-height: .4;
+display: block;
+margin: 0 0 20px;
+}
+.stars { display: inline-flex; gap: 3px; margin: 10px 0 16px; font-size: 21px; color: #d6d9df; }
+.stars .on { color: var(--amber); }
+.footer-col {
+border-left: 1px solid #ffffff1a;
+/* border-right: 1px solid #ffffff1a; */
+padding: 40px 0 40px 40px;
+}
+.testi-quote {
+color: var(--ink);
+font-size: 17px;
+line-height: 1.7;
+}
+.testi-author-card { display: flex; align-items: center; gap: 14px;
+background: #fff; border: 1px solid var(--line); border-radius: 20px; padding: 20px 24px;
+box-shadow: 0 10px 45px rgba(0, 0, 0, 0.04); }
+.testi-author-card img { width: 52px; height: 52px; border-radius: 11px; object-fit: cover; }
+.testi-author-card strong { font-size: 17px; font-weight: 600; display: block; }
+.testi-author-card span { font-family: var(--display); font-size: 13px; color: var(--ink-2); }
+.testi-more { text-align: center; margin-top: 40px; }
+.testi-more a { font-size: 17px; color: var(--ink); border-bottom: 1px solid var(--ink); padding-bottom: 4px; }
+/* ==========================================================================
+CONTACT PAGE
+========================================================================== */
+.ct-hero { border-radius: 30px;
+margin: 0 30px;
+padding: 200px 0 110px;
+position: relative;
+overflow: hidden;
+margin-top: 30px; }
+.ct-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.ct-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.8), rgba(6,20,49,0.25) 60%); }
+.ct-hero-inner { position: relative; z-index: 2; color: #fff; }
+.ct-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 10px; }
+.ct-hero-inner p { color: rgba(255,255,255,0.85); }
+.ct-methods { position: relative; z-index: 2; display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 35px; }
+.ct-method { text-align: center;
+box-shadow: var(--shadow);
+background: #fff;
+border-radius: 16px;
+padding: 35px; }
+.ct-icon { display: inline-grid; }
+.ct-icon img {
+object-fit: contain;
+width: 45px;
+height: 45px;
+margin-bottom: 20px;
+}
+.ct-field {
+position: relative;
+margin: 0 0 20px;
+}
+.ct-field .ct-icon {
+position: absolute;
+top: 30%;
+left: 14px;
+}
+.ct-textarea-wrap .ct-icon {
+top: 22px;
+}
+.ct-field .ct-icon img {
+margin: 0;
+width: auto;
+height: auto;
+}
+.ct-label { display: block; font-size: 18px; color: #000000; }
+.ct-method strong { font-size: 24px; font-weight: 600; }
+.ct-connect {
+background: #FFFFFF;
+}
+.ct-connect-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 60px; align-items: stretch; }
+.ct-photo { border-radius: 20px; overflow: hidden; }
+.ct-photo img { object-fit: cover;
+width: 100%;
+height: 680px;
+border-radius: 20px; }
+.ct-form-sub { color: black; margin: 10px 0 30px; font-size: 18px; }
+.ct-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
+.ct-form label { display: block; margin-bottom: 18px; }
+.ct-form label span { display: block; font-size: 14px; color: var(--ink-2); margin-bottom: 8px; }
+.ct-form input, .ct-form textarea {
+border: 1px solid var(--line);
+width: 100%;
+font: inherit;
+color: var(--ink);
+background: #fff;
+border: 1px solid #dbdbdb;
+border-radius: 8px;
+padding: 17px 0 17px 50px;
+font-size: 17px;
+transition: border-color .2s;
+}
+.ct-form input:focus, .ct-form textarea:focus { outline: none; border-color: var(--blue); }
+.ct-form textarea { resize: vertical; }
+.ct-submit { margin-top: 6px; }
+.ct-submit:disabled { opacity: .6; cursor: not-allowed; }
+.ct-error { color: #c0392b; font-size: 15px; margin: -6px 0 14px; }
+
+/* Thank-you popup */
+.ct-thanks-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ display: grid;
+ place-items: center;
+ padding: 20px;
+ background: rgba(13, 20, 33, 0.55);
+ backdrop-filter: blur(4px);
+ animation: ct-fade .25s ease;
+}
+.ct-thanks-card {
+ position: relative;
+ width: 100%;
+ max-width: 440px;
+ background: #fff;
+ border-radius: 22px;
+ padding: 46px 40px 40px;
+ text-align: center;
+ box-shadow: var(--shadow);
+ animation: ct-pop .32s cubic-bezier(.2, .9, .3, 1.2);
+}
+.ct-thanks-close {
+ position: absolute;
+ top: 16px;
+ right: 18px;
+ width: 34px;
+ height: 34px;
+ border: none;
+ background: transparent;
+ font-size: 26px;
+ line-height: 1;
+ color: var(--ink-3);
+ cursor: pointer;
+ border-radius: 50%;
+ transition: background .2s, color .2s;
+}
+.ct-thanks-close:hover { background: #f1f3f6; color: var(--ink); }
+.ct-thanks-check {
+ width: 76px;
+ height: 76px;
+ margin: 0 auto 22px;
+ display: grid;
+ place-items: center;
+ border-radius: 50%;
+ color: #fff;
+ background: var(--green-bright);
+ box-shadow: 0 10px 26px rgba(29, 158, 22, 0.35);
+ animation: ct-pop .45s .1s both cubic-bezier(.2, .9, .3, 1.4);
+}
+.ct-thanks-card h3 {
+ font-family: var(--serif);
+ font-size: 30px;
+ margin-bottom: 10px;
+ color: var(--ink);
+}
+.ct-thanks-card p {
+ color: var(--ink-2);
+ font-size: 16px;
+ line-height: 1.5;
+ margin-bottom: 28px;
+}
+.ct-thanks-card .btn { min-width: 140px; justify-content: center; }
+@keyframes ct-fade { from { opacity: 0; } to { opacity: 1; } }
+@keyframes ct-pop {
+ from { opacity: 0; transform: scale(.9) translateY(10px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+.ct-spiritual .section-title { margin-top: 12px; }
+.ct-feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; margin-top: 44px; }
+.ct-feature { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 35px; box-shadow: var(--shadow-sm); }
+.ct-feature-icon { background: var(--bg-blue);
+border-radius: 12px;
+place-items: center;
+width: 56px;
+height: 56px;
+margin-bottom: 30px;
+font-size: 10px;
+display: inline-grid; }
+.ct-feature-icon img { width: 24px; height: 24px; object-fit: contain; }
+.ct-feature h4 { font-family: var(--serif); font-size: 24px; margin-bottom: 6px; }
+.ct-feature p { font-size: 14px; color: var(--ink-2); }
+.ct-feature.active { background: var(--blue); color: #fff; }
+.ct-feature.active h4 { color: #fff; }
+.ct-feature.active p { color: rgba(255,255,255,0.9); }
+.ct-feature.active .ct-feature-icon { background: rgba(255,255,255,0.2); }
+@media (max-width: 900px) {
+.ct-methods { grid-template-columns: 1fr; }
+.ct-connect-grid { grid-template-columns: 1fr; }
+.ct-feature-grid { grid-template-columns: 1fr 1fr; }
+.ct-row { grid-template-columns: 1fr; }
+}
+/* ==========================================================================
+FAQ PAGE
+========================================================================== */
+.faq-hero { border-radius: 30px;
+margin: 30px 30px 0px;
+padding: 200px 0 120px;
+position: relative;
+overflow: hidden; }
+.faq-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.faq-hero-overlay { position: absolute;
+inset: 0;
+background: linear-gradient(260deg, rgba(0, 0, 0, 0) 21.44%, rgba(0, 94, 201, 0.78) 75.8%, #0066DA 101.07%);
+border-radius: 30px 0px 0px 30px; }
+.faq-hero-inner { position: relative; z-index: 2; color: #fff; }
+.faq-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 8px; }
+.faq-hero-inner p { color: #ffffffd9;
+ width: 40%; }
+.faq-grid { display: grid; grid-template-columns: 0.7fr 1.3fr; gap: 50px; align-items: start; }
+.faq-photo { border-radius: 18px; overflow: hidden; position: sticky; top: 110px; }
+.faq-photo img { width: 100%; height: 560px; object-fit: cover; }
+.faq-list { display: flex; flex-direction: column; gap: 14px; }
+.faq-item { background: #f7f9fc; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; transition: background 0.2s; }
+.faq-item.open { background: var(--bg-blue); border-color: #c9e0ff; }
+.faq-q { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 20px 24px; text-align: left; font-size: 18px; font-weight: 500; }
+.faq-item.open .faq-q { font-weight: 600; }
+.faq-q i { font-style: normal; font-size: 22px; color: var(--blue); flex-shrink: 0; }
+.faq-a { padding: 0 24px 22px; font-size: 15px; color: var(--ink-2); line-height: 1.7; }
+@media (max-width: 900px) {
+.faq-grid { grid-template-columns: 1fr; }
+.faq-photo { position: static; }
+.faq-photo img { height: 280px; }
+}
+/* ==========================================================================
+PRIVACY POLICY PAGE
+========================================================================== */
+.pp-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
+.pp-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.pp-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.25) 60%); }
+.pp-hero-inner { position: relative; z-index: 2; color: #fff; }
+.pp-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 8px; }
+.pp-hero-inner p { color: rgba(255,255,255,0.85); }
+.pp-intro { max-width: 920px; font-size: 18px; color: var(--ink-2); line-height: 1.8; margin-bottom: 56px; }
+.pp-grid { display: grid; grid-template-columns: 300px 1fr; gap: 56px; align-items: start; }
+.pp-toc {
+padding: 28px;
+position: sticky;
+top: 70px;
+background: #EFF7FF;
+border-radius: 15px; }
+.pp-toc li:hover {
+}
+.pp-toc h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 6px; }
+.pp-updated { font-size: 14px; color: var(--ink-3); margin-bottom: 18px; }
+.pp-toc ol { list-style: none; counter-reset: none; }
+.pp-toc li { margin-bottom: 12px; }
+.pp-toc li:hover {
+}
+.pp-toc a { font-size: 15px; color: var(--ink-2); transition: color 0.2s; }
+.pp-toc a:hover { color: var(--blue); }
+.pp-content section { margin-bottom: 40px; scroll-margin-top: 100px; }
+.pp-content h2 { font-family: var(--serif); font-size: 20px; margin-bottom: 14px; }
+.pp-content h4 { font-size: 16px; font-weight: 600; margin: 18px 0 10px; }
+.pp-content p { font-size: 16px; color: var(--ink-2); line-height: 1.8; margin-bottom: 12px; }
+.pp-content ul { list-style: disc; padding-left: 22px; }
+.pp-content li { font-size: 16px; color: var(--ink-2); line-height: 1.9; }
+.pp-contact { background: #f7f9fc; border-radius: 12px; padding: 20px 24px; }
+.pp-contact strong { color: var(--ink); }
+@media (max-width: 900px) {
+.pp-grid { grid-template-columns: 1fr; }
+.pp-toc { position: static; }
+}
+/* ==========================================================================
+ABOUT PAGE
+========================================================================== */
+.ab-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 200px 0 110px; }
+.ab-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.ab-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.8), rgba(6,20,49,0.25) 60%); }
+.ab-hero-inner { position: relative; z-index: 2; color: #fff; }
+.ab-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 10px; }
+.ab-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
+.ab-who-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 56px; align-items: center; }
+.ab-who-media { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
+.ab-who-media > img { width: 100%; height: 440px; object-fit: cover; border-radius: 16px; margin-top: 40px; }
+.ab-who-second { position: relative; }
+.ab-who-second img { width: 100%; height: 440px; object-fit: cover; border-radius: 16px; }
+.ab-badge { position: absolute; left: 14px; bottom: 14px; right: 14px; background: rgba(8,16,32,0.85); color: #fff; border-radius: 12px; padding: 16px; display: block; gap: 12px; align-items: center; font-size: 13px; backdrop-filter: blur(4px); }
+.ab-badge strong { font-family: var(--serif);
+font-size: 30px;
+width: 100%;
+display: flex;
+font-weight: 400;
+}
+.ab-who-text .section-title { margin-bottom: 22px; }
+.ab-who-text p { color: var(--ink-2); margin-bottom: 16px; font-size: 16px; line-height: 1.8; }
+.ab-who-text strong { color: var(--ink-2); }
+.ab-leaders { grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ gap: 70px;
+ margin-bottom: 70px;
+ display: grid; }
+.ab-leader { background: #fff; border: 1px solid var(--line); border-radius: 18px; box-shadow: var(--shadow-sm); }
+.ab-leader.right { flex-direction: row-reverse; text-align: right; }
+.ab-leader img {
+ object-fit: cover;
+ border-radius: 0px;
+ width: 61%;
+ height: 440px;
+ background: #EFEFF1;
+ margin: 0 auto;
+ }
+ .ab-leader .teamwrap {
+ background: #efeff1;
+ }
+.ab-leader-info h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 4px; }
+.ab-leader-info span { display: block; color: var(--ink-2); font-size: 14px; margin-bottom: 8px;
+ text-align: justify;
+}
+.ab-leader-info {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 30px 30px 20px;
+}
+.leadcont {
+
+}
+.ab-leader-info a { font-size: 14px; color: black; }
+.ab-leader-emblem { text-align: center; }
+.ab-leader-emblem img {
+ border-radius: 50%;
+ width: 120px;
+ height: 120px;
+ margin: 0 auto 8px;
+ }
+.ab-leader-emblem span { font-family: var(--display); font-weight: 700; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2); }
+.ab-india-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 36px; }
+.ab-team-grid { display: flex; gap: 24px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; padding-bottom: 6px; }
+.ab-team-grid::-webkit-scrollbar { display: none; }
+.ab-member {
+ scroll-snap-align: start;
+ text-align: center;
+ padding-bottom: 22px;
+ border: 1px solid var(--line);
+ background: rgb(247, 249, 252);
+ border-radius: 18px;
+ flex: 0 0 calc(25% - 18px);
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 15px;
+
+ }
+ .leadcontwrap {
+ justify-content: space-between;
+ align-items: self-end;
+ padding: 8px 20px;
+ display: flex;
+ }
+ .leadcont {
+ text-align: justify;
+
+ }
+.ab-member-photo { background: #D9D9D9;
+ height: 390px; }
+.ab-member-photo img {
+ object-fit: cover;
+ width: 80%;
+ height: 100%;
+ margin: 0 auto;
+ }
+.ab-member h4 { font-family: var(--serif); font-size: 18px; margin: 18px 0 4px; }
+.ab-member span {
+ color: var(--ink-2);
+ margin-bottom: -1px;
+ font-size: 14px;
+ display: block;
+ }
+.ab-member a { color: #0e0e0e;
+ font-size: 11px; }
+.ab-experts .why-head { max-width: 600px; }
+.ab-expert-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin: 44px 0 22px; }
+.ab-expert { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 30px; display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
+.ab-expert strong { font-family: var(--serif); font-size: 40px; display: block; margin-bottom: 8px; font-weight: 200; }
+.ab-expert h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 10px; }
+.ab-expert p { font-size: 14px; color: var(--ink-2); }
+.ab-expert-icon { width: 40px; height: 40px; object-fit: contain; flex-shrink: 0; }
+.ab-badge-grid {
+ grid-template-columns: repeat(4, 1fr);
+ gap: 24px;
+ padding: 36px;
+ display: grid;
+ box-sizing: border-box;
+ background: #FFFFFF;
+ border: 1px solid #E4E4E4;
+ box-shadow: 15px 20px 45px rgba(233, 233, 233, 0.25);
+ border-radius: 15px;
+ }
+.ab-cert { text-align: left; border-right: 1px solid var(--line); padding-right: 20px; display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
+.ab-cert-icon { width: 34px; height: 34px; object-fit: contain; flex-shrink: 0; }
+.ab-cert:last-child { border-right: none; }
+.ab-cert strong { font-family: var(--serif);
+ font-size: 29px;
+ display: block;
+ font-weight: 200; }
+.ab-cert span { font-family: var(--display); font-size: 14px; color: var(--ink-2); }
+.ab-steps {
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0px;
+ margin-top: 0px;
+ display: grid;
+}
+.ab-step {
+ border: 1px solid var(--line);
+ background: #fff;
+ border-radius: 0;
+ min-height: 170px;
+ padding: 26px;
+ position: relative;
+}
+.ab-step strong {
+ font-weight: 100;
+ font-family: var(--serif); font-size: 30px; color: var(--ink-3); display: block; margin-bottom: 14px; }
+.ab-step h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 8px; }
+.ab-step p { font-size: 16px; color: var(--ink-2); }
+.ab-step-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 16px; opacity: 0; }
+.ab-step-card { padding: 0; overflow: hidden; }
+.ab-step-cardimg { display: block; width: 100%; height: 100%; object-fit: cover; }
+@media (max-width: 1000px) {
+.ab-who-grid { grid-template-columns: 1fr; }
+.ab-leaders { grid-template-columns: 1fr; }
+.ab-expert-grid, .ab-badge-grid, .ab-steps { grid-template-columns: 1fr 1fr; }
+.ab-member { flex-basis: calc(50% - 12px); }
+.ab-cert { border-right: none; }
+}
+@media (max-width: 640px) {
+.ab-expert-grid, .ab-badge-grid, .ab-steps, .ab-who-media { grid-template-columns: 1fr; }
+.ab-member { flex-basis: 82%; }
+}
+/* ==========================================================================
+PROJECTS PAGE
+========================================================================== */
+.pr-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
+.pr-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.pr-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.3) 60%); }
+.pr-hero-inner { position: relative; z-index: 2; color: #fff; max-width: 760px; }
+.pr-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
+.pr-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
+.pr-across { position: relative; padding: 100px 0; overflow: hidden; }
+.pr-across-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.pr-across-overlay {
+ background: linear-gradient(#06122880, #06122859);
+ position: absolute;
+ inset: 0;
+ position: absolute;
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0) 3.21%, rgba(0, 0, 0, 0.595) 59.51%, rgba(0, 0, 0, 0.7) 100%);
+ transform: rotate(180deg);
+ }
+.pr-across-inner { position: relative; z-index: 2; color: #fff; }
+.pr-across-inner h2 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
+.pr-across-inner > p { max-width: 520px; color: rgba(255,255,255,0.85); margin-bottom: 44px; }
+.pr-across-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
+.pr-across-card {
+ background: #fffffff5;
+ border-radius: 4px;
+ justify-content: space-between;
+ align-items: end;
+ padding: 22px 22px;
+ display: flex;
+ }
+.pr-across-card strong { font-family: var(--serif);
+ font-size: 36px;
+ color: black;
+ display: block;
+ font-weight: 300; }
+.pr-across-card span { font-size: 13px; color: var(--ink-2); }
+.pr-across-card h4 { font-family: var(--serif);
+ margin-top: 8px;
+ font-size: 23px;
+ color: black; }
+.pr-across-icon { font-size: 26px; }
+@media (max-width: 900px) {
+.pr-across-cards { grid-template-columns: 1fr 1fr; }
+}
+/* ==========================================================================
+BLOG LIST + BLOG DETAIL
+========================================================================== */
+.bl-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
+.bl-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
+.bl-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.3) 60%); }
+.bl-hero-inner { position: relative; z-index: 2; color: #fff; max-width: 640px; }
+.bl-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
+.bl-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
+.bl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
+.bl-card { padding: 22px;
+transition: box-shadow .2s, transform .2s;
+box-sizing: border-box;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px; }
+.bl-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-3px); }
+.bl-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 46px; }
+.bl-tag { font-family: var(--display); font-size: 13px; background: #eef2f7; color: #000000; padding: 6px 16px; border-radius: 100px; }
+.bl-thumb { width: 150px; height: 104px; border-radius: 10px; overflow: hidden; }
+.bl-thumb img { width: 100%; height: 100%; object-fit: cover; }
+.bl-meta span {
+display: flex;
+align-items: center;
+gap: 10px;
+}
+.bl-meta { color: var(--ink-2);
+gap: 18px;
+margin-bottom: 42px;
+font-size: 15px;
+display: flex;
+justify-content: space-between; }
+.bl-card h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 10px; }
+.bl-card p { font-size: 14px; color: var(--ink-2); margin-bottom: 18px; }
+.bl-more { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; color: var(--blue); }
+/* detail */
+.bd-grid { display: grid; grid-template-columns: 1fr 360px; gap: 50px; align-items: start; }
+.bd-feature { width: 100%; height: 420px; object-fit: cover; border-radius: 18px; margin-bottom: 24px; }
+.bd-meta { display: flex; gap: 22px; font-size: 14px; color: var(--ink-2); margin-bottom: 16px; }
+.bd-title { font-family: var(--serif); font-size: 40px; margin-bottom: 18px; }
+.bd-main p { font-size: 16px; color: var(--ink-2); line-height: 1.85; margin-bottom: 18px; }
+.bd-split { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 26px; align-items: center; margin: 14px 0 30px; }
+.bd-split img { width: 100%; height: 280px; object-fit: cover; border-radius: 14px; }
+.bd-split h3 { font-family: var(--serif); font-size: 26px; margin-bottom: 16px; }
+.bd-checks { display: flex; flex-direction: column; gap: 12px; }
+.bd-checks li { font-size: 15px; color: var(--ink); }
+.bd-quote { margin: 10px 0 34px;
+padding: 60px 40px;
+position: relative;
+background: #FFFFFF;
+box-shadow: 0px 0px 60px rgba(0, 0, 0, 0.05);
+border-radius: 20px; }
+.bd-quote-mark { }
+.bd-quote p { color: var(--ink);
+margin: 38px 0 39px;
+font-size: 20px;
+line-height: 1.5; }
+.bd-quote footer strong { font-size: 22px;
+display: block; }
+.bd-quote footer span { font-size: 14px; color: var(--ink-2); }
+.bd-h2 { font-family: var(--serif); font-size: 32px; margin: 8px 0 18px; }
+.bd-tagrow { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin: 30px 0; padding: 24px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); flex-wrap: wrap; }
+.bd-tags { display: flex; flex-wrap: wrap; gap: 10px; }
+.bd-tags span { font-size: 14px; background: var(--bg-blue); color: var(--blue); padding: 7px 16px; border-radius: 100px; }
+.bd-share { display: flex; gap: 10px; }
+.bd-share a { width: 36px; height: 36px; border-radius: 50%; background: #eef2f7; display: grid; place-items: center; font-size: 13px; color: var(--ink-2); }
+.bd-comment {
+margin-top: 34px;
+background: #FFFFFF;
+border: 1px solid #EFEFEF;
+box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
+border-radius: 20px;
+padding: 30px 40px;
+}
+.bd-comment-note { font-size: 14px; color: var(--ink-2); margin-bottom: 20px; }
+.bd-comment-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px; }
+.bd-comment input, .bd-comment textarea { width: 100%; border: 1px solid var(--line); border-radius: 10px; padding: 13px 16px; font: inherit; font-size: 15px; background: #fbfcfe; }
+.bd-comment textarea { resize: vertical; margin-bottom: 14px; }
+.bd-check { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--ink-2); margin-bottom: 18px; }
+.bd-check input { width: auto; }
+.bd-side { display: flex; flex-direction: column; gap: 24px; position: sticky; top: 100px; }
+.bd-widget { padding: 24px;
+background: #FFFFFF;
+box-shadow: 0px 0px 60px rgba(0, 0, 0, 0.05);
+border-radius: 10px; }
+.bd-widget h4 { font-family: var(--serif);
+margin-bottom: 21px;
+font-size: 24px;
+border-bottom: 1px solid #80808029;
+padding-bottom: 30px;}
+.abt-hero {
+padding-top: 320px;
+}
+.bd-search { display: flex; gap: 8px; }
+.bd-search input { flex: 1; border: 1px solid var(--line); border-radius: 30px; padding: 12px 14px; font: inherit; background: #fff; }
+.bd-search button { position: absolute;
+color: #000000;
+border-radius: 10px;
+width: 36px;
+font-size: 26px;
+right: 34px; }
+.bd-pop { display: flex; gap: 14px; align-items: center; margin-bottom: 16px; }
+.bd-pop img { object-fit: cover;
+border-radius: 8px;
+flex-shrink: 0;
+width: 70px;
+height: 73px; }
+.bd-pop span img {
+width: 15px;
+height: 15px;
+object-fit: fill;
+border-radius: 0;
+margin-right: 6px;
+}
+.bd-pop span { display: flex; font-size: 12px; color: var(--ink-3); margin-bottom: 4px; }
+.bd-pop strong { font-size: 15px; font-weight: 500; }
+.bd-cat { display: block; font-size: 15px; color: black; padding: 10px 0; border-bottom: 1px solid var(--line); }
+.bd-cat:last-child { border-bottom: none; }
+@media (max-width: 1000px) {
+.bl-grid { grid-template-columns: 1fr 1fr; }
+.bd-grid { grid-template-columns: 1fr; }
+.bd-side { position: static; }
+}
+@media (max-width: 640px) {
+.bl-grid { grid-template-columns: 1fr; }
+.bd-split, .bd-comment-row { grid-template-columns: 1fr; }
+}
+/* ============================ SERVICES PAGE ============================== */
+.sv-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; margin-top: 44px; }
+.sv-card { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 30px; transition: box-shadow 0.2s, transform 0.2s; }
+.sv-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-4px); }
+.sv-icon { display: inline-grid; place-items: center; width: 52px; height: 52px; border-radius: 14px; background: var(--bg-blue); font-size: 22px; margin-bottom: 20px; }
+.sv-card h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 10px; }
+.sv-card p { font-size: 14px; color: var(--ink-2); }
+@media (max-width: 1000px) { .sv-grid { grid-template-columns: 1fr 1fr; } }
+@media (max-width: 600px) { .sv-grid { grid-template-columns: 1fr; } }
+/* ----------------------- home responsive -------------------------------- */
+@media (max-width: 1100px) {
+.proj-big, .proj-small, .companies-grid, .testi-grid { grid-template-columns: 1fr 1fr; }
+.why-grid { grid-template-columns: 1fr; }
+.why-visual { height: 360px; }
+.journey-card { grid-template-columns: 100px 1fr; }
+.journey-middle { grid-template-columns: 1fr; }
+}
+@media (max-width: 768px) {
+.hero { height: 620px; margin: 0 12px; }
+.hero-content { padding: 0 26px; }
+.hero-content h1 { font-size: 32px; }
+.hero-note { display: none; }
+.walk-tabs, .stage-cards { grid-template-columns: 1fr 1fr; }
+.walk-stage, .virtual-stage { height: 380px; }
+.proj-big, .proj-small, .companies-grid, .testi-grid { grid-template-columns: 1fr; }
+.testi-col:nth-child(2) { margin-top: 0; }
+.dream-title { font-size: 40px; }
+.dream-inner { padding-bottom: 60px; }
+.journey-card { grid-template-columns: 1fr; }
+.journey-image { width: 100%; height: 180px; }
+.head-aside { align-items: flex-start; }
+}
\ No newline at end of file
diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts
new file mode 100644
index 0000000..8328a8e
--- /dev/null
+++ b/src/app/api/contact/route.ts
@@ -0,0 +1,82 @@
+import { NextResponse } from "next/server";
+
+type ContactBody = {
+ firstName?: string;
+ lastName?: string;
+ email?: string;
+ mobile?: string;
+ subject?: string;
+ message?: string;
+};
+
+export async function POST(request: Request) {
+ const token = process.env.TEABLE_API_TOKEN;
+ const tableId = process.env.TEABLE_TABLE_ID;
+
+ if (!token || !tableId) {
+ return NextResponse.json(
+ { error: "Server is not configured for submissions." },
+ { status: 500 }
+ );
+ }
+
+ let body: ContactBody;
+ try {
+ body = await request.json();
+ } catch {
+ return NextResponse.json({ error: "Invalid request body." }, { status: 400 });
+ }
+
+ const firstName = (body.firstName ?? "").trim();
+ const lastName = (body.lastName ?? "").trim();
+ const email = (body.email ?? "").trim();
+ const mobile = (body.mobile ?? "").trim();
+ const subject = (body.subject ?? "").trim();
+ const message = (body.message ?? "").trim();
+
+ const name = `${firstName} ${lastName}`.trim();
+
+ if (!name || !email || !message) {
+ return NextResponse.json(
+ { error: "Name, email and message are required." },
+ { status: 400 }
+ );
+ }
+
+ // The Teable table is job-application shaped; map contact details onto the
+ // free-text fields that fit. Subject + mobile are folded into why_role so no
+ // information is lost.
+ const why_role = [
+ subject && `Subject: ${subject}`,
+ mobile && `Mobile: ${mobile}`,
+ message,
+ ]
+ .filter(Boolean)
+ .join("\n");
+
+ const res = await fetch(
+ `https://app.teable.ai/api/table/${tableId}/record`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ fieldKeyType: "name",
+ records: [{ fields: { name, email, why_role } }],
+ }),
+ }
+ );
+
+ if (!res.ok) {
+ const detail = await res.text();
+ console.error("Teable error:", res.status, detail);
+ return NextResponse.json(
+ { error: "Could not save your message. Please try again." },
+ { status: 502 }
+ );
+ }
+
+ return NextResponse.json({ ok: true });
+}
diff --git a/src/app/contact/ContactForm.tsx b/src/app/contact/ContactForm.tsx
deleted file mode 100644
index 84783af..0000000
--- a/src/app/contact/ContactForm.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"use client";
-
-export default function ContactForm() {
- return (
-
- );
-}
diff --git a/src/app/style.css b/src/app/style.css
deleted file mode 100644
index b6b0455..0000000
--- a/src/app/style.css
+++ /dev/null
@@ -1,1424 +0,0 @@
-/* ==========================================================================
- YAMUNA EXPRESSWAY AUTHORITY — single global stylesheet
- Recreated from Figma "yamunaexpress wayauthority". No frameworks.
- ========================================================================== */
-
- @import url("https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Heebo:wght@300;400;500;600;700&family=Parkinsans:wght@300..800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Philosopher:wght@400;700&family=Gloock&display=swap");
-
- /* ------------------------------- tokens --------------------------------- */
- :root {
- --serif: "Libre Baskerville", "Baskerville Old Face", Georgia, serif;
- --body: "Heebo", system-ui, sans-serif;
- --ui: "Plus Jakarta Sans", "Heebo", sans-serif;
- --display: "Parkinsans", "Heebo", sans-serif;
-
- --ink: #151515;
- --ink-2: #636669;
- --ink-3: #a8acb4;
- --line: #e6e8ec;
- --bg: #ffffff;
- --bg-soft: #f5faff;
- --bg-blue: #e4f1ff;
-
- --green: #003616;
- --green-bright: #1d9e16;
- --navy: #0b1220;
- --navy-2: #0f1419;
- --blue: #0077ff;
- --blue-2: #2261ff;
- --amber: #ffc107;
- --orange: #f45700;
-
- --container: 1370px;
- --radius: 18px;
- --radius-lg: 28px;
- --shadow: 0 24px 60px rgba(13, 20, 33, 0.10);
- --shadow-sm: 0 12px 30px rgba(13, 20, 33, 0.08);
- }
-
- /* ------------------------------- reset ---------------------------------- */
- *,
- *::before,
- *::after { box-sizing: border-box; }
-
- * { margin: 0; padding: 0; }
-
- html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
-
- body {
- transition: all 0.3s ease 0s;
- font-family: var(--body);
- color: var(--ink);
- background: var(--bg);
- line-height: 1.6;
- -webkit-font-smoothing: antialiased;
-
- text-rendering: optimizeLegibility;
- overflow-x: clip;
- }
-
- img { max-width: 100%; display: block; }
-
- a { color: inherit; text-decoration: none; }
-
- button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
-
- ul { list-style: none; }
-
- h1, h2, h3, h4, h5 { font-weight: 400; line-height: 1.12; }
-
- /* ----------------------------- helpers ---------------------------------- */
- .container {
- width: 100%;
- max-width: var(--container);
- margin: 0 auto;
- }
-
- .section { padding: 90px 0; }
-
- .eyebrow {
- font-family: var(--body);
- font-size: 12px;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: var(--blue);
- font-weight: 500;
- margin-bottom: 18px;
- }
-
- .h-serif { font-family: var(--serif); }
-
- .section-title {
- font-family: var(--serif);
- font-size: 50px;
- line-height: 1.08;
- color: var(--ink);
- letter-spacing: -0.01em;
- }
-
- .section-head {
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- gap: 40px;
- margin-bottom: 48px;
- }
-
- .section-head .lead { max-width: 420px; color: var(--ink-2); font-size: 16px; }
-
- /* buttons */
- .btn {
- display: inline-flex;
- align-items: center;
- gap: 10px;
- font-family: var(--ui);
- font-weight: 500;
- font-size: 15px;
- padding: 14px 26px;
- border-radius: 100px;
- transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
- }
- .btn:hover { transform: translateY(-2px); }
- .btn-dark { background: var(--navy); color: #fff; }
- .btn-light { background: #fff; color: var(--ink); box-shadow: var(--shadow-sm); }
- .btn-ghost { border: 1px solid rgba(255, 255, 255, 0.5); color: #fff; }
- .btn .arrow { font-size: 16px; }
-
- /* ============================== HEADER ================================== */
- .site-header {
- position: absolute;
- top: 40px;
- left: 0;
- right: 0;
- z-index: 50;
- padding: 28px 0;
- }
- .header-inner {
- max-width: 1835px;
- margin: 0 auto;
- padding: 0 50px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 24px;
- }
- .brand { display: flex; align-items: center; gap: 12px; color: #fff; }
- .brand-emblem {
- width: 54px; height: 54px; border-radius: 50%;
- display: grid; place-items: center;
- background: rgba(255, 255, 255, 0.12);
- overflow: hidden;
- }
- .brand-emblem img { width: 100%; height: 100%; object-fit: cover; }
- .brand-text {
- font-family: var(--display);
- font-size: 15px;
- font-weight: 700;
- line-height: 1.05;
- letter-spacing: 0.06em;
- text-transform: uppercase;
- }
-
- .nav-pill {
- display: flex;
- align-items: center;
- gap: 6px;
- background: rgba(255, 255, 255, 0.92);
- padding: 8px;
- border-radius: 100px;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
- }
- .nav-link {
- font-family: var(--display);
- font-size: 15px;
- color: #1d2430;
- padding: 11px 20px;
- border-radius: 100px;
- white-space: nowrap;
- transition: background 0.2s ease, color 0.2s ease;
- }
- .nav-link:hover { background: #eef2f7; }
- .nav-link.active { background: var(--navy); color: #fff; }
-
- .header-right { display: flex; align-items: center; gap: 14px; }
- .social-dots { display: flex; gap: 8px; }
- .social-dots .dot {
- width: 38px; height: 38px; border-radius: 50%;
- display: grid; place-items: center;
- background: rgba(255, 255, 255, 0.16);
- color: #fff; font-size: 13px;
- border: 1px solid rgba(255, 255, 255, 0.25);
- transition: background 0.2s ease;
- }
- .social-dots .dot:hover { background: rgba(255, 255, 255, 0.32); }
- .social-dots .dot img { width: 18px; height: 18px; object-fit: contain; }
- .btn-login {
- font-family: var(--display);
- background: #fff; color: var(--ink);
- padding: 11px 26px; border-radius: 100px; font-size: 15px;
- transition: transform 0.2s ease;
- }
- .btn-login:hover { transform: translateY(-2px); }
- .menu-btn {
- width: 46px; height: 46px; border-radius: 14px;
- background: #fff; display: none;
- flex-direction: column; gap: 4px; align-items: center; justify-content: center;
- }
- .menu-btn span { width: 18px; height: 2px; background: var(--ink); display: block; }
-
- /* ============================== FOOTER ================================== */
- .site-footer {
- position: relative;
- margin-top: 0;
- background: #050c1b;
- color: #d7dce5;
- overflow: hidden;
- padding-top: 220px;
- }
- .site-footer::before {
- content: "";
- position: absolute;
- inset: 0;
- background:
- linear-gradient(180deg, rgba(5, 12, 27, 0.35) 0%, rgba(5, 12, 27, 0.9) 70%, #050c1b 100%),
- url("/img/f0abba0eed40b4f2211d403ef3f57addd1f216f6.png") center top / cover no-repeat;
- opacity: 0.9;
- }
- .footer-watermark {
- position: absolute;
- top: 70px;
- left: 50%;
- transform: translateX(-50%);
- font-family: var(--display);
- font-weight: 700;
- font-size: 220px;
- line-height: 1;
- color: rgba(255, 255, 255, 0.08);
- white-space: nowrap;
- pointer-events: none;
- }
- .footer-inner { position: relative; z-index: 2; padding-bottom: 0; }
- .footer-grid {
- display: grid;
- grid-template-columns: 1.6fr 1fr 1fr 1.3fr;
- gap: 40px;
- padding: 48px 0 40px;
- border-top: 1px solid rgba(255, 255, 255, 0.08);
- }
- .footer-brand { max-width: 360px; }
- .footer-emblem {
- width: 58px; height: 58px; border-radius: 50%;
- display: block; overflow: hidden; margin-bottom: 20px;
- background: rgba(255, 255, 255, 0.1);
- }
- .footer-emblem img { width: 100%; height: 100%; object-fit: cover; }
- .footer-brand p { font-size: 15px; color: #aab2c0; line-height: 1.7; }
- .footer-reg { margin-top: 20px; font-size: 13px; color: #7e8798; font-family: var(--ui); }
- .footer-col h4 {
- font-family: var(--ui);
- font-weight: 600;
- font-size: 17px;
- color: #fff;
- margin-bottom: 22px;
- letter-spacing: 0.02em;
- }
- .footer-col a, .footer-col p {
- display: block;
- font-family: var(--ui);
- font-size: 15px;
- color: #aab2c0;
- margin-bottom: 14px;
- transition: color 0.2s ease;
- }
- .footer-col a:hover { color: #fff; }
- .footer-bottom {
- position: relative;
- z-index: 2;
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 20px;
- padding: 24px 0 34px;
- border-top: 1px solid rgba(255, 255, 255, 0.08);
- }
- .footer-bottom p { font-family: var(--ui); font-size: 14px; color: #8a93a3; }
- .footer-social { display: flex; gap: 12px; }
- .footer-social a {
- width: 34px; height: 34px; border-radius: 50%;
- display: grid; place-items: center;
- background: rgba(255, 255, 255, 0.08);
- font-size: 13px; color: #cfd6e2;
- transition: background 0.2s ease;
- }
- .footer-social a:hover { background: rgba(255, 255, 255, 0.2); }
-
- /* ----------------------- generic inner page hero ------------------------ */
- .page-hero {
- position: relative;
- padding: 220px 0 90px;
- color: #fff;
- text-align: center;
- background: linear-gradient(180deg, rgba(5, 12, 27, 0.55), rgba(5, 12, 27, 0.75));
- }
- .page-hero h1 { font-family: var(--serif); font-size: 64px; margin-bottom: 18px; }
- .page-hero p { max-width: 620px; margin: 0 auto; color: rgba(255, 255, 255, 0.85); }
- .page-hero .crumbs { font-family: var(--ui); font-size: 14px; letter-spacing: 0.05em; margin-bottom: 22px; color: rgba(255,255,255,0.8); }
-
- /* ===================== carousels (drag / swipe) ========================= */
- .plots-rail, .ab-team-grid { cursor: grab; }
- .is-dragging { cursor: grabbing; user-select: none; scroll-behavior: auto !important; }
- .is-dragging * { pointer-events: none; }
-
- /* ============================ RESPONSIVE ================================ */
- @media (max-width: 1200px) {
- .section-title { font-size: 40px; }
- .footer-grid { grid-template-columns: 1fr 1fr; }
- .footer-brand { grid-column: 1 / -1; max-width: none; }
- }
- @media (max-width: 1024px) {
- .nav-pill {
- position: absolute;
- top: 88px; right: 50px;
- flex-direction: column;
- align-items: stretch;
- display: none;
- }
- .nav-pill.is-open { display: flex; }
- .menu-btn { display: flex; }
- .social-dots { display: none; }
- }
- @media (max-width: 768px) {
- .container { padding: 0 20px; }
- .header-inner { padding: 0 20px; }
- .section { padding: 60px 0; }
- .section-title { font-size: 32px; }
- .section-head { flex-direction: column; align-items: flex-start; gap: 16px; }
- .footer-grid { grid-template-columns: 1fr; }
- .footer-watermark { font-size: 90px; top: 90px; }
- .footer-bottom { flex-direction: column; align-items: flex-start; }
- .page-hero h1 { font-size: 40px; }
- }
-
- /* ==========================================================================
- HOME PAGE
- ========================================================================== */
-
- /* ------------------------------ hero ------------------------------------ */
- .hero {
- position: relative;
- height: 900px;
- margin: 30px 30px;
- border-radius: 30px;
- overflow: hidden;
- display: flex;
- align-items: center;
- }
- .hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .hero-overlay {
- position: absolute;
- inset: 0;
- background: linear-gradient(258.88deg, rgba(0, 0, 0, 0) 22.04%, #0077FF 104.42%);
- border-radius: 30px 0px 0px 30px;
- }
- .hero-content {
- z-index: 2;
- color: #fff;
- max-width: 820px;
- padding: 0 90px;
- position: relative;
- margin-top: 290px;
- }
- .hero-content h1 {
- font-family: var(--serif);
- font-size: 50px;
- line-height: 1.15;
- margin-bottom: 22px;
- }
- .hero-content h1 span { display: inline-block;
- font-weight: 600;}
- .hero-sub {color: #ffffffe0;
- margin-bottom: 34px;
- font-size: 18px;
- font-weight: 400; }
- .hero-cta { font-size: 15px; }
- .hero-note {
- position: absolute;
- right: 60px;
- bottom: 60px;
- z-index: 2;
- text-align: right;
- color: rgba(255, 255, 255, 0.9);
- font-size: 18px;
- font-weight: 500;
- line-height: 1.4;
- }
-
- /* --------------------------- feature strip ------------------------------ */
- .feature-strip {
- border: 1px solid var(--line);
- background: #fff;
- margin-top: 4px;
- padding: 26px 0;
- overflow: hidden;
- margin: 0 30px;
- border-radius: 11px;
- }
- .feature-track {
- display: flex;
- gap: 60px;
- width: max-content;
- animation: marquee 32s linear infinite;
- white-space: nowrap;
- }
- .feature-item {
- font-family: var(--ui);
- color: var(--ink);
- align-items: center;
- gap: 14px;
- font-size: 16px;
- font-weight: 500;
- display: inline-flex;
- text-transform: uppercase;
- }
- .dot-sq { width: 7px; height: 7px; background: var(--ink-3); display: inline-block; transform: rotate(45deg); }
- @keyframes marquee {
- from { transform: translateX(0); }
- to { transform: translateX(-50%); }
- }
-
- /* ------------------------------ plots ----------------------------------- */
- .head-aside { align-items: flex-end;
- gap: 92px;
- display: flex; }
- .plots-rail {
- display: flex;
- gap: 24px;
- overflow-x: auto;
- padding-bottom: 14px;
- scroll-snap-type: x mandatory;
- scrollbar-width: none;
- }
- .plots-rail::-webkit-scrollbar { display: none; }
- .villa-card {
- scroll-snap-align: start;
- flex: 0 0 calc((100% - 48px) / 3);
- transition: box-shadow .25s, transform .25s;
- overflow: hidden;
- position: relative;
- background: #FFFFFF;
- border: 1px solid #EFEFEF;
- border-radius: 20px;
- }
- .walkthrough .lead {
- text-align: right;
- }
- .villa-card:hover { box-shadow: var(--shadow); transform: translateY(-4px); }
- .villa-media {
- border-radius: 16px;
- height: 260px;
- padding: 10px;
- position: relative; }
- .villa-media img { object-fit: cover;
- width: 100%;
- height: 100%;
- border-radius: 12px; }
- .villa-tags { gap: 8px;
- display: flex;
- position: absolute;
- bottom: 21px;
- left: 22px; }
- .villa-tags span {
- color: #000000;
- -webkit-backdrop-filter: blur(4px);
- backdrop-filter: blur(4px);
- background: #ffffff;
- border-radius: 38px;
- padding: 5px 10px;
- font-size: 11px;
- }
- .villa-floor {
- right: 12px; bottom: 12px;
- width: 30px; height: 30px; border-radius: 8px;
- background: rgba(10, 16, 28, 0.78); color: #fff;
- display: grid; place-items: center; font-size: 12px;
- }
- .villa-body {align-items: self-start;
- display: flex;
- padding: 10px 20px 0;
- justify-content: space-between;}
- .villa-body h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 8px; }
- .villa-desc { font-size: 13px; color: var(--ink-2); margin-bottom: 18px; }
- .villa-foot { display: flex; justify-content: space-between; align-items: flex-end; padding-top: 16px; }
- .villa-price { font-family: var(--serif);
- flex-direction: column;
- font-size: 18px;
- display: flex;
- text-align: end; }
- .villa-price small { font-family: var(--body); font-size: 11px; color: var(--ink-3); letter-spacing: 0.1em; }
- .villa-months { color: var(--ink-2);
- font-size: 12px;
- position: absolute;
- top: 20px;
- color: #fff;
- -webkit-backdrop-filter: blur(4px);
- backdrop-filter: blur(4px);
- background: #0a101cc7;
- border-radius: 38px;
- padding: 6px 16px;
- font-size: 11px;
- right: 20px; }
-
- .plots-pager { display: flex; align-items: center; gap: 24px; margin-top: 36px; }
- .pager-count { font-size: 14px; color: var(--ink); font-weight: 600; }
- .pager-line { flex: 1; height: 2px; background: var(--line); position: relative; }
- .pager-line span { position: absolute; left: 0; top: 0; height: 100%; width: 18%; background: var(--blue); }
- .pager-btns { display: flex; gap: 10px; }
- .pager-btns button { width: 42px; height: 42px; border: 1px solid var(--line); border-radius: 50%; color: var(--ink-2); transition: background 0.2s; }
- .pager-btns button:hover { background: #f2f5f9; }
-
- /* --------------------------- walkthrough -------------------------------- */
- .walk-stage {
- height: 620px;
- position: relative;
- overflow: hidden;
- border-radius: 20px 20px 0 0;
- }
- .walk-stage {
- position: relative;
- }
-
- .corner {
- position: absolute;
- width: 65px;
- height: 65px;
- z-index: 5;
- }
-
- /* Top Left */
- .corner.tl {
- top: 30px;
- left: 30px;
- border-top: 1px solid #fff;
- border-left: 1px solid #fff;
- }
-
- /* Top Right */
- .corner.tr {
- top: 30px;
- right: 30px;
- border-top: 1px solid #fff;
- border-right: 1px solid #fff;
- }
-
- /* Bottom Left */
- .corner.bl {
- bottom: 30px;
- left: 30px;
- border-bottom: 1px solid #fff;
- border-left: 1px solid #fff;
- }
-
- /* Bottom Right */
- .corner.br {
- bottom: 30px;
- right: 30px;
- border-bottom: 1px solid #fff;
- border-right: 1px solid #fff;
- }
- .walk-stage img { width: 100%; height: 100%; object-fit: cover; }
- .walk-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.55)); }
- .play-btn {
- position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
- width: 84px; height: 84px; border-radius: 50%;
- background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(6px);
- border: 1px solid rgba(255, 255, 255, 0.5);
- display: grid; place-items: center; color: #fff; font-size: 26px;
- transition: transform 0.2s, background 0.2s;
- }
- .play-btn:hover { transform: translate(-50%, -50%) scale(1.08); background: rgba(255, 255, 255, 0.4); }
- .walk-caption { color: #fff;
- font-family: var(--serif);
- font-size: 23px;
- position: absolute;
- bottom: 48px;
- left: 66px; }
- .walk-meta { color: #fffc;
- font-size: 14px;
- position: absolute;
- bottom: 50px;
- right: 56px; }
- .walk-tabs {
- box-shadow: var(--shadow-sm);
- background: #fff;
- border-radius: 0px;
- grid-template-columns: repeat(4, 1fr);
- gap: 0;
- margin-top: 0px;
- display: grid;
- overflow: hidden; }
- .walk-tab { padding: 22px; font-weight: 500; font-size: 16px; color: var(--ink-2); transition: background 0.2s, color 0.2s; border-right: 1px solid var(--line); }
- .walk-tab:last-child { border-right: none; }
- .walk-tab.active { background: var(--blue-2); color: #fff; font-weight: 600; }
-
- /* ----------------- dream realized (scroll-driven stages) ---------------- */
- /* Tall track: the inner pins to the viewport while you scroll through it,
- and the active stage (0–3) re-treats the same villa view. */
- .dream { position: relative; height: 440vh; }
- .dream-sticky {
- position: sticky; top: 0; height: 95vh;
- overflow: hidden; display: flex; flex-direction: column;
- }
-.dream-inner:before {
- content: "";
- border: 1px solid #fff;
- border-image: initial;
- width: 92%;
- height: 81%;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
- /* one image that morphs across every stage */
- .dream-bg {
- position: absolute; inset: 0; z-index: 0;
- width: 100%; height: 100%; object-fit: cover;
- transition: filter 1s ease, transform 1.4s ease;
- will-change: filter, transform;
- }
- .dream[data-stage="0"] .dream-bg { transform: scale(1.12);
- background: linear-gradient(180deg, rgba(0, 0, 0, 0) 3.21%, rgba(0, 0, 0, 0.34) 72.64%, rgba(0, 0, 0, 0.5) 100%); }
- .dream[data-stage="1"] .dream-bg { filter: brightness(0.5) sepia(1) hue-rotate(168deg) saturate(6) contrast(1.2); transform: scale(1.08); }
- .dream[data-stage="2"] .dream-bg { filter: sepia(0.5) saturate(1.15) brightness(0.9) contrast(1.05); transform: scale(1.04); }
- .dream[data-stage="3"] .dream-bg { filter: none; transform: scale(1); }
-
- /* blueprint grid — fades in only for the blueprint stage */
- .dream-grid {
- position: absolute; inset: 0; z-index: 1; pointer-events: none;
- opacity: 0; transition: opacity 0.9s ease; mix-blend-mode: screen;
- background-image:
- linear-gradient(rgba(120, 180, 255, 0.4) 1px, transparent 1px),
- linear-gradient(90deg, rgba(120, 180, 255, 0.4) 1px, transparent 1px);
- background-size: 46px 46px;
- }
- .dream[data-stage="1"] .dream-grid { opacity: 1; }
-
- /* construction scanlines — fade in only for the construction stage */
- .dream-scan {
- position: absolute; inset: 0; z-index: 1; pointer-events: none;
- opacity: 0; transition: opacity 0.9s ease;
- background: repeating-linear-gradient(0deg, rgba(255, 196, 84, 0.07) 0 3px, transparent 3px 7px);
- }
- .dream[data-stage="2"] .dream-scan { opacity: 1; }
-
- .dream-overlay {
- position: absolute; inset: 0; z-index: 2; pointer-events: none;
- background: linear-gradient(180deg, rgba(6, 18, 40, 0.5), rgba(6, 18, 40, 0.12) 38%, rgba(6, 18, 40, 0.7));
- }
-
- /* copy block — vertically centred in the space above the cards */
- .dream-inner::before {
- content: "";
- width: 100px;
- height: 2px;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- border-width: 1px;
- border-style: solid;
- border-color: rgb(255, 255, 255);
- border-image: initial;
-}
- .dream-inner {
- z-index: 3;
- flex: 1;
- margin-top: 180px;
- display: flex;
- position: relative;
- }
- .dream-inner-in { align-items: flex-start;
- gap: 28px;
- padding: 0 24px;
- display: flex;
- min-width: 86%; }
- .dream-rail { display: flex; flex-direction: column; gap: 14px; padding-top: 8px; }
- .dream-dot {
- width: 14px; height: 14px; border-radius: 50%;
- border: 1.5px solid rgba(255, 255, 255, 0.45); position: relative; transition: all 0.4s ease;
- }
- .dream-dot > span {
- position: absolute; inset: 3px; border-radius: 50%; background: #fff;
- transform: scale(0); transition: transform 0.4s ease;
- }
- .dream-dot.done { border-color: rgba(255, 255, 255, 0.75); }
- .dream-dot.done > span { transform: scale(1); opacity: 0.6; }
- .dream-dot.on { border-color: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.14); }
- .dream-dot.on > span { transform: scale(1); }
-
- .dream-copy { color: #fff; max-width: 560px; animation: dreamFade 0.7s ease; }
- @keyframes dreamFade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }
- .dream-stage { font-size: 13px; letter-spacing: 0.22em; margin-bottom: 14px; opacity: 0.85; }
- .dream-title { font-family: var(--serif); margin-bottom: 16px; font-size: 54px; line-height: 1.05; }
- .dream-sub { font-size: 16px; color: rgba(255, 255, 255, 0.88); line-height: 1.6; }
-
- /* bottom progress strip */
- .stage-cards.active h4 {
- color: white;
- }
- .dream-rail {
- display: none;
- }
- .stage-cards {
- z-index: 3;
- background: #ffffff1f;
- grid-template-columns: repeat(4, 1fr);
- gap: 1px;
- display: grid;
- position: relative;
- top: -3px;
- }
- .stage-card {
- color: #000000;
- background: #ffffff;
- padding: 24px 26px;
- transition: background .5s, color .5s;
- }
- .stage-num { font-size: 12px; letter-spacing: 0.2em; opacity: 0.7; }
- .stage-card h4 { font-family: var(--serif); font-size: 19px; margin: 8px 0; color: black; }
- .stage-card p { font-size: 12.5px; line-height: 1.5; color: black; }
- .stage-card.past { opacity: 0.9; }
- .stage-card.active { background: var(--blue); color: #fff; }
- .stage-card.active h4 { color: #fff; }
- .stage-card.active p { color: rgba(255, 255, 255, 0.92); }
- .stage-card.active .stage-num { opacity: 0.95; }
-
- /* ---------------------------- landmark ---------------------------------- */
- .landmark {
- background: #F5FAFF;
- }
- .proj-body {
- padding-top: 20px;
- }
- .proj-tabs { display: inline-flex; gap: 8px; background: #eef2f7; padding: 6px; border-radius: 100px; }
- .proj-tabs button { padding: 12px 24px; border-radius: 100px; font-size: 15px; color: var(--ink-2); font-weight: 500; }
- .proj-tabs button.active { background: var(--blue-2); color: #fff; }
- .proj-big { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px; }
- .proj-small { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
- .proj-card { border: 1px solid var(--line);
- background: #fff;
- border-radius: 20px;
- overflow: hidden;
- background: #FFFFFF;
-border: 1px solid #EFEFEF;
-box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
-border-radius: 20px;
- padding: 25px; }
- .proj-card:hover { box-shadow: var(--shadow-sm); }
- .proj-media { position: relative; }
- .proj-card.big .proj-media { height: 320px;
- border-radius: 20px; }
- .proj-card.small .proj-media { height: 220px; }
- .proj-media img { width: 100%; height: 100%; object-fit: cover; border-radius: 20px; }
- .proj-badge { position: absolute; top: 16px; right: 16px; background: var(--green-bright); color: #fff; font-size: 13px; font-weight: 600; padding: 7px 16px; border-radius: 100px; }
- .proj-badge.dark { background: var(--navy); }
-
- .proj-body h3 { font-family: var(--serif); font-size: 26px; margin-bottom: 12px; }
- .proj-card.small .proj-body h3 { font-size: 22px; }
- .proj-body p {
- color: var(--ink-2);
- margin-bottom: 22px;
- font-size: 16px;
- width: 80%;
- }
- .proj-stats {
- border-top: 1px solid var(--line);
- gap: 28px;
- padding-top: 18px;
- display: flex;
- justify-content: space-between;
- }
- .proj-stats div { display: flex; flex-direction: column; }
- .proj-stats strong { font-family: var(--serif); font-weight: 400; font-size: 22px; }
- .proj-card.small .proj-stats strong { font-size: 20px; }
- .proj-stats span { font-size: 13px; color: var(--ink-2); }
-
- /* ----------------------------- journey ---------------------------------- */
- .journey-list { display: flex; flex-direction: column; gap: 16px; }
- .journey-card {
- display: grid;
- grid-template-columns: 300px 1fr;
- gap: 30px;
- align-items: stretch;
- padding: 22px;
- cursor: pointer;
- position: relative;
- transition: box-shadow 0.2s, border-color 0.2s;
-background: #FFFFFF;
-border: 1px solid #EFEFEF;
-box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
-border-radius: 20px;
- }
- .journey-card.active { box-shadow: var(--shadow-sm); border-color: #d3dae6; }
- .journey-image {
- border-radius: 14px;
- width: 100%;
- height: 380px;
- overflow: hidden;}
- .journey-image img { width: 100%; height: 100%; object-fit: cover; }
- .journey-content { display: flex; flex-direction: column; }
- .journey-top {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- border-bottom: 1px solid #8080802e;
- padding-bottom: 20px;
- margin-bottom: 8px; }
- .journey-week { color: var(--blue);
- letter-spacing: 3px;
- font-size: 13px;
- font-weight: 600;
- text-transform: uppercase; }
- .journey-top h3 { font-family: var(--serif);
- margin-top: 8px;
- font-size: 26px; }
- .journey-step { display: flex; flex-direction: column; align-items: flex-end; }
- .journey-step span { font-size: 11px; letter-spacing: 2px; text-transform: uppercase; color: var(--ink-3); }
- .journey-step strong { font-size: 22px; color: var(--ink-3); font-weight: 300; }
- .journey-middle { display: grid; grid-template-columns: 1.4fr 1fr; gap: 30px; flex: 1; }
- .journey-sub { font-family: var(--serif);
- color: var(--ink);
- font-size: 21px;
- line-height: 1.4;
- width: 87%;
- margin-top: 50px; }
- .journey-body { color: var(--ink-2);
- margin-top: 32px;
- font-size: 16px;
- width: 80%;
- padding-bottom: 20px; }
- .journey-right-content { display: flex; flex-direction: column; gap: 10px; }
- .journey-item {
- color: #000000;
- border-bottom: 1px dashed var(--line);
- justify-content: space-between;
- padding-bottom: 8px;
- font-size: 16px;
- display: flex;
- }
- .journey-item .day { color: #070707; }
- .journey-thumb { position: absolute; bottom: 22px; right: 22px; }
-
- /* ---- pinned scroll reveal (Your Journey) ---- */
- .journey-pin.is-pinned { position: relative; }
- .journey-pin.is-pinned .journey-sticky {
- position: sticky;
- top: 0;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- justify-content: center; }
- .journey-pin.is-pinned .journey-list { position: relative; display: block; }
- .journey-pin.is-pinned .journey-card {
- position: absolute;
- left: 0;
- right: 0;
- top: 0;
- z-index: 1;
- opacity: 0;
- visibility: hidden;
- transform: translateY(34px) scale(0.985);
- transition: opacity 0.6s ease, transform 0.6s ease;
- pointer-events: none; }
- .journey-pin.is-pinned .journey-card.active {
- position: relative;
- z-index: 3;
- opacity: 1;
- visibility: visible;
- transform: none;
- pointer-events: auto; }
- /* next card peeks out below the active one, like a stacked deck */
- .journey-pin.is-pinned .journey-card.next {
- z-index: 2;
- opacity: 0.6;
- visibility: visible;
- transform: translateY(70px) scale(0.96); }
- .journey-pin.is-pinned .journey-item {
- opacity: 0;
- transform: translateX(20px);
- transition: opacity 0.5s ease, transform 0.5s ease; }
- .journey-pin.is-pinned .journey-item.show { opacity: 1; transform: none; }
-
- /* ----------------------------- virtual ---------------------------------- */
- .virtual-head { text-align: center; max-width: 640px; margin: 0 auto 44px; }
- .virtual-head .lead { color: var(--ink-2); margin-top: 16px; }
- .virtual-stage { position: relative; border-radius: var(--radius-lg); overflow: hidden; height: 560px; max-width: 1100px; margin: 0 auto; }
- .virtual-stage img { width: 100%; height: 100%; object-fit: cover; }
- .virtual-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.05), rgba(0,0,0,0.5)); }
- .virtual-caption { position: absolute; left: 40px; bottom: 90px; color: #fff; }
- .virtual-caption h3 { font-size: 28px; font-weight: 500; margin-bottom: 6px; }
- .virtual-caption p { font-size: 15px; color: rgba(255, 255, 255, 0.85); }
- .virtual-features {
- position: absolute; left: 0; right: 0; bottom: 0;
- display: flex; justify-content: center; gap: 34px; flex-wrap: wrap;
- background: rgba(255, 255, 255, 0.96); padding: 18px;
- }
- .virtual-features span { font-size: 14px; color: var(--ink); display: inline-flex; align-items: center; gap: 8px; }
- .virtual-features span::first-letter { color: var(--green-bright); }
-
- /* ---------------------------- companies --------------------------------- */
- .companies-head { text-align: center; margin-bottom: 50px; }
- .companies-head .lead.center { margin: 16px auto 0; max-width: 560px; color: var(--ink-2); }
- .companies-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
- .company-card { padding: 32px;
- transition: box-shadow .2s, transform .2s;
- background: #FFFFFF;
- border: 1px solid #EFEFEF;
- box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
- border-radius: 20px; }
- .company-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-4px); }
- .company-logo { height: 60px; display: flex; align-items: center; margin-bottom: 24px; }
- .company-logo img { max-height: 56px; width: auto; }
- .company-mark { font-family: var(--display); font-weight: 700; color: var(--orange); font-size: 20px; }
- .company-country { font-size: 13px; color: var(--ink-3); }
- .company-card h3 { font-family: var(--serif); font-size: 26px; margin: 6px 0 16px; }
- .company-tag { font-size: 17px; color: var(--ink); margin-bottom: 12px; }
- .company-desc { font-size: 14px; color: var(--ink-2); margin-bottom: 26px; }
- .company-stats { display: flex; gap: 40px; padding-top: 22px; border-top: 1px solid var(--line); }
- .company-stats strong { font-family: var(--serif); font-weight: 400; font-size: 20px; display: block; }
- .company-stats span { font-size: 13px; color: var(--ink-2); }
-
- /* ---------------------------- why choose -------------------------------- */
- .why-head { max-width: 560px; margin-bottom: 50px; }
- .why-head .lead { margin-top: 16px; color: var(--ink-2); }
- .why-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 40px; align-items: start; }
- .why-visual { border-radius: 20px; overflow: hidden; height: 620px; }
- .why-tall { width: 100%; height: 100%; object-fit: cover; }
- .why-list { position: relative; }
- .why-wide { width: 100%; height: 240px; object-fit: cover; border-radius: 18px; margin-bottom: 20px; }
- .why-row { display: flex; gap: 20px; align-items: flex-start; padding: 22px 4px; border-bottom: 1px solid var(--line); cursor: pointer; }
- .why-no { font-family: var(--display); font-size: 16px; font-weight: 500; color: var(--ink-3); }
- .why-text h4 { font-family: var(--serif); font-size: 22px; }
- .why-text p { margin-top: 8px; font-size: 15px; color: var(--ink-2); }
- .why-row.open .why-text h4 { color: var(--blue); }
-
- /* --------------------------- showcase ----------------------------------- */
- .showcase { position: relative; padding: 90px 0; overflow: hidden; }
- .showcase-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .showcase-overlay { position: absolute; inset: 0; background: rgba(6, 16, 35, 0.45); }
- .showcase-brand { position: absolute; left: 50px; bottom: 40px; z-index: 3; color: #fff; font-family: var(--display); font-weight: 700; font-size: 26px; letter-spacing: 0.04em; }
- .showcase-card { position: relative; z-index: 2; width: min(880px, 86%); margin: 0 auto; border-radius: 22px; overflow: hidden; box-shadow: 0 40px 80px rgba(0,0,0,0.4); }
- .showcase-card > img { width: 100%; height: 460px; object-fit: cover; }
- .showcase-bar { position: absolute; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 22px 28px; color: #fff; }
- .sc-menu, .sc-contact { font-size: 13px; letter-spacing: 0.08em; }
- .sc-logo { font-family: var(--display); font-weight: 700; font-size: 22px; letter-spacing: 0.1em; }
- .showcase-caption { position: absolute; left: 32px; bottom: 32px; color: #fff; }
- .showcase-caption h3 { font-family: var(--serif); font-size: 34px; line-height: 1.1; }
-
- /* -------------------------- testimonials -------------------------------- */
- .testi-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: start; }
- .testi-card { background: #fff; border: 1px solid var(--line); border-radius: 20px; padding: 30px; }
- .testi-card:nth-child(2) { margin-top: 40px; }
- .quote-mark { font-family: var(--serif); font-size: 50px; color: var(--ink-3); line-height: 0.4; display: block; height: 26px; }
- .stars { display: inline-flex; gap: 3px; margin: 10px 0 16px; font-size: 15px; color: #d6d9df; }
- .stars .on { color: var(--amber); }
- .testi-quote { font-size: 15px; color: var(--ink); line-height: 1.7; margin-bottom: 26px; }
- .testi-author { display: flex; align-items: center; gap: 14px; padding-top: 20px; border-top: 1px solid var(--line); }
- .testi-author img { width: 46px; height: 46px; border-radius: 50%; object-fit: cover; }
- .testi-author strong { font-size: 17px; font-weight: 600; display: block; }
- .testi-author span { font-family: var(--display); font-size: 13px; color: var(--ink-2); }
- .testi-more { text-align: center; margin-top: 40px; }
- .testi-more a { font-size: 17px; color: var(--ink); border-bottom: 1px solid var(--ink); padding-bottom: 4px; }
-
- /* ==========================================================================
- CONTACT PAGE
- ========================================================================== */
- .ct-hero { border-radius: 30px;
- margin: 0 30px;
- padding: 200px 0 110px;
- position: relative;
- overflow: hidden;
- margin-top: 30px; }
- .ct-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .ct-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.8), rgba(6,20,49,0.25) 60%); }
- .ct-hero-inner { position: relative; z-index: 2; color: #fff; }
- .ct-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 10px; }
- .ct-hero-inner p { color: rgba(255,255,255,0.85); }
- .ct-methods { position: relative; z-index: 2; display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 35px; }
- .ct-method { text-align: center;
- box-shadow: var(--shadow);
- background: #fff;
- border-radius: 16px;
- padding: 35px; }
- .ct-icon { display: inline-grid; }
- .ct-icon img {
- object-fit: contain;
- width: 45px;
- height: 45px;
- margin-bottom: 20px;
-}
-.ct-field {
- position: relative;
- margin: 0 0 20px;
-}
-.ct-field .ct-icon {
- position: absolute;
- top: 30%;
- left: 14px;
-}
-.ct-textarea-wrap .ct-icon {
- top: 22px;
-}
-.ct-field .ct-icon img {
- margin: 0;
- width: auto;
- height: auto;
-}
- .ct-label { display: block; font-size: 18px; color: #000000; }
- .ct-method strong { font-size: 24px; font-weight: 600; }
- .ct-connect {
- background: #FFFFFF;
- }
- .ct-connect-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 60px; align-items: stretch; }
- .ct-photo { border-radius: 20px; overflow: hidden; }
- .ct-photo img { object-fit: cover;
- width: 100%;
- height: 680px;
- border-radius: 20px; }
- .ct-form-sub { color: black; margin: 10px 0 30px; font-size: 18px; }
- .ct-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
- .ct-form label { display: block; margin-bottom: 18px; }
- .ct-form label span { display: block; font-size: 14px; color: var(--ink-2); margin-bottom: 8px; }
- .ct-form input, .ct-form textarea {
- border: 1px solid var(--line);
- width: 100%;
- font: inherit;
- color: var(--ink);
- background: #fff;
- border: 1px solid #dbdbdb;
- border-radius: 8px;
- padding: 17px 0 17px 50px;
- font-size: 17px;
- transition: border-color .2s;
- }
- .ct-form input:focus, .ct-form textarea:focus { outline: none; border-color: var(--blue); }
- .ct-form textarea { resize: vertical; }
- .ct-submit { margin-top: 6px; }
-
- .ct-spiritual .section-title { margin-top: 12px; }
- .ct-feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; margin-top: 44px; }
- .ct-feature { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 35px; box-shadow: var(--shadow-sm); }
- .ct-feature-icon { background: var(--bg-blue);
- border-radius: 12px;
- place-items: center;
- width: 56px;
- height: 56px;
- margin-bottom: 30px;
- font-size: 10px;
- display: inline-grid; }
- .ct-feature-icon img { width: 24px; height: 24px; object-fit: contain; }
- .ct-feature h4 { font-family: var(--serif); font-size: 24px; margin-bottom: 6px; }
- .ct-feature p { font-size: 14px; color: var(--ink-2); }
- .ct-feature.active { background: var(--blue); color: #fff; }
- .ct-feature.active h4 { color: #fff; }
- .ct-feature.active p { color: rgba(255,255,255,0.9); }
- .ct-feature.active .ct-feature-icon { background: rgba(255,255,255,0.2); }
-
- @media (max-width: 900px) {
- .ct-methods { grid-template-columns: 1fr; }
- .ct-connect-grid { grid-template-columns: 1fr; }
- .ct-feature-grid { grid-template-columns: 1fr 1fr; }
- .ct-row { grid-template-columns: 1fr; }
- }
-
- /* ==========================================================================
- FAQ PAGE
- ========================================================================== */
- .faq-hero { border-radius: 30px;
- margin: 30px 30px 0px;
- padding: 200px 0 120px;
- position: relative;
- overflow: hidden; }
- .faq-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .faq-hero-overlay { position: absolute;
- inset: 0;
- background: linear-gradient(260deg, rgba(0, 0, 0, 0) 21.44%, rgba(0, 94, 201, 0.78) 75.8%, #0066DA 101.07%);
- border-radius: 30px 0px 0px 30px; }
- .faq-hero-inner { position: relative; z-index: 2; color: #fff; }
- .faq-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 8px; }
- .faq-hero-inner p { color: rgba(255,255,255,0.85); }
- .faq-grid { display: grid; grid-template-columns: 0.7fr 1.3fr; gap: 50px; align-items: start; }
- .faq-photo { border-radius: 18px; overflow: hidden; position: sticky; top: 110px; }
- .faq-photo img { width: 100%; height: 560px; object-fit: cover; }
- .faq-list { display: flex; flex-direction: column; gap: 14px; }
- .faq-item { background: #f7f9fc; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; transition: background 0.2s; }
- .faq-item.open { background: var(--bg-blue); border-color: #c9e0ff; }
- .faq-q { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 20px 24px; text-align: left; font-size: 18px; font-weight: 500; }
- .faq-item.open .faq-q { font-weight: 600; }
- .faq-q i { font-style: normal; font-size: 22px; color: var(--blue); flex-shrink: 0; }
- .faq-a { padding: 0 24px 22px; font-size: 15px; color: var(--ink-2); line-height: 1.7; }
-
- @media (max-width: 900px) {
- .faq-grid { grid-template-columns: 1fr; }
- .faq-photo { position: static; }
- .faq-photo img { height: 280px; }
- }
-
- /* ==========================================================================
- PRIVACY POLICY PAGE
- ========================================================================== */
- .pp-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
- .pp-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .pp-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.25) 60%); }
- .pp-hero-inner { position: relative; z-index: 2; color: #fff; }
- .pp-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 8px; }
- .pp-hero-inner p { color: rgba(255,255,255,0.85); }
- .pp-intro { max-width: 920px; font-size: 18px; color: var(--ink-2); line-height: 1.8; margin-bottom: 56px; }
- .pp-grid { display: grid; grid-template-columns: 300px 1fr; gap: 56px; align-items: start; }
- .pp-toc {
- padding: 28px;
- position: sticky;
- top: 70px;
- background: #EFF7FF;
- border-radius: 15px; }
- .pp-toc li:hover {
-
- }
- .pp-toc h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 6px; }
- .pp-updated { font-size: 14px; color: var(--ink-3); margin-bottom: 18px; }
- .pp-toc ol { list-style: none; counter-reset: none; }
- .pp-toc li { margin-bottom: 12px; }
- .pp-toc li:hover {
-
- }
- .pp-toc a { font-size: 15px; color: var(--ink-2); transition: color 0.2s; }
- .pp-toc a:hover { color: var(--blue); }
- .pp-content section { margin-bottom: 40px; scroll-margin-top: 100px; }
- .pp-content h2 { font-family: var(--serif); font-size: 20px; margin-bottom: 14px; }
- .pp-content h4 { font-size: 16px; font-weight: 600; margin: 18px 0 10px; }
- .pp-content p { font-size: 16px; color: var(--ink-2); line-height: 1.8; margin-bottom: 12px; }
- .pp-content ul { list-style: disc; padding-left: 22px; }
- .pp-content li { font-size: 16px; color: var(--ink-2); line-height: 1.9; }
- .pp-contact { background: #f7f9fc; border-radius: 12px; padding: 20px 24px; }
- .pp-contact strong { color: var(--ink); }
-
- @media (max-width: 900px) {
- .pp-grid { grid-template-columns: 1fr; }
- .pp-toc { position: static; }
- }
-
- /* ==========================================================================
- ABOUT PAGE
- ========================================================================== */
- .ab-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 200px 0 110px; }
- .ab-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .ab-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.8), rgba(6,20,49,0.25) 60%); }
- .ab-hero-inner { position: relative; z-index: 2; color: #fff; }
- .ab-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 10px; }
- .ab-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
-
- .ab-who-grid { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: 56px; align-items: center; }
- .ab-who-media { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; align-items: start; }
- .ab-who-media > img { width: 100%; height: 440px; object-fit: cover; border-radius: 16px; margin-top: 40px; }
- .ab-who-second { position: relative; }
- .ab-who-second img { width: 100%; height: 440px; object-fit: cover; border-radius: 16px; }
- .ab-badge { position: absolute; left: 14px; bottom: 14px; right: 14px; background: rgba(8,16,32,0.85); color: #fff; border-radius: 12px; padding: 16px; display: block; gap: 12px; align-items: center; font-size: 13px; backdrop-filter: blur(4px); }
- .ab-badge strong { font-family: var(--serif);
- font-size: 30px;
- width: 100%;
- display: flex;
- font-weight: 400;
- }
- .ab-who-text .section-title { margin-bottom: 22px; }
- .ab-who-text p { color: var(--ink-2); margin-bottom: 16px; font-size: 16px; line-height: 1.8; }
- .ab-who-text strong { color: var(--ink-2); }
-
- .ab-leaders { display: grid; grid-template-columns: 1fr auto 1fr; gap: 30px; align-items: center; margin-bottom: 70px; }
- .ab-leader { display: flex; gap: 20px; align-items: flex-end; background: #fff; border: 1px solid var(--line); border-radius: 18px; padding: 18px; box-shadow: var(--shadow-sm); }
- .ab-leader.right { flex-direction: row-reverse; text-align: right; }
- .ab-leader img { width: 150px; height: 180px; object-fit: cover; border-radius: 12px; }
- .ab-leader-info h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 4px; }
- .ab-leader-info span { display: block; color: var(--ink-2); font-size: 14px; margin-bottom: 8px; }
- .ab-leader-info a { font-size: 14px; color: var(--blue); }
- .ab-leader-emblem { text-align: center; }
- .ab-leader-emblem img { width: 90px; height: 90px; border-radius: 50%; margin: 0 auto 8px; }
- .ab-leader-emblem span { font-family: var(--display); font-weight: 700; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-2); }
-
- .ab-india-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 36px; }
- .ab-team-grid { display: flex; gap: 24px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; padding-bottom: 6px; }
- .ab-team-grid::-webkit-scrollbar { display: none; }
- .ab-member { flex: 0 0 calc(25% - 18px); scroll-snap-align: start; background: #f7f9fc; border: 1px solid var(--line); border-radius: 18px; overflow: hidden; padding-bottom: 22px; text-align: center; }
- .ab-member-photo { height: 300px; background: #e9eef5; }
- .ab-member-photo img { width: 100%; height: 100%; object-fit: cover; }
- .ab-member h4 { font-family: var(--serif); font-size: 24px; margin: 18px 0 4px; }
- .ab-member span { display: block; color: var(--ink-2); font-size: 14px; margin-bottom: 6px; }
- .ab-member a { font-size: 13px; color: var(--ink-3); }
-
- .ab-experts .why-head { max-width: 600px; }
- .ab-expert-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin: 44px 0 22px; }
- .ab-expert { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 30px; display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
- .ab-expert strong { font-family: var(--serif); font-size: 40px; display: block; margin-bottom: 8px; }
- .ab-expert h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 10px; }
- .ab-expert p { font-size: 14px; color: var(--ink-2); }
- .ab-expert-icon { color: var(--ink-3); font-size: 24px; }
- .ab-badge-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; background: #f7f9fc; border-radius: 18px; padding: 36px; }
- .ab-cert { text-align: left; border-right: 1px solid var(--line); padding-right: 20px; }
- .ab-cert:last-child { border-right: none; }
- .ab-cert strong { font-family: var(--serif); font-size: 36px; display: block; }
- .ab-cert span { font-family: var(--display); font-size: 14px; color: var(--ink-2); }
-
- .ab-steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 40px; }
- .ab-step { position: relative; background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 26px; min-height: 170px; }
- .ab-step strong { font-family: var(--serif); font-size: 30px; color: var(--ink-3); display: block; margin-bottom: 14px; }
- .ab-step h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 8px; }
- .ab-step p { font-size: 14px; color: var(--ink-2); }
- .ab-step-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 16px; opacity: 0; }
-
- @media (max-width: 1000px) {
- .ab-who-grid { grid-template-columns: 1fr; }
- .ab-leaders { grid-template-columns: 1fr; }
- .ab-expert-grid, .ab-badge-grid, .ab-steps { grid-template-columns: 1fr 1fr; }
- .ab-member { flex-basis: calc(50% - 12px); }
- .ab-cert { border-right: none; }
- }
- @media (max-width: 640px) {
- .ab-expert-grid, .ab-badge-grid, .ab-steps, .ab-who-media { grid-template-columns: 1fr; }
- .ab-member { flex-basis: 82%; }
- }
-
- /* ==========================================================================
- PROJECTS PAGE
- ========================================================================== */
- .pr-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
- .pr-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .pr-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.3) 60%); }
- .pr-hero-inner { position: relative; z-index: 2; color: #fff; max-width: 760px; }
- .pr-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
- .pr-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
-
- .pr-across { position: relative; padding: 100px 0; overflow: hidden; }
- .pr-across-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .pr-across-overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(6,18,40,0.5), rgba(6,18,40,0.35)); }
- .pr-across-inner { position: relative; z-index: 2; color: #fff; }
- .pr-across-inner h2 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
- .pr-across-inner > p { max-width: 520px; color: rgba(255,255,255,0.85); margin-bottom: 44px; }
- .pr-across-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
- .pr-across-card { background: rgba(255,255,255,0.96); border-radius: 14px; padding: 22px; display: flex; justify-content: space-between; align-items: center; }
- .pr-across-card strong { font-family: var(--serif); font-size: 36px; display: block; }
- .pr-across-card span { font-size: 13px; color: var(--ink-2); }
- .pr-across-card h4 { font-family: var(--serif); font-size: 20px; margin-top: 4px; }
- .pr-across-icon { font-size: 26px; }
-
- @media (max-width: 900px) {
- .pr-across-cards { grid-template-columns: 1fr 1fr; }
- }
-
- /* ==========================================================================
- BLOG LIST + BLOG DETAIL
- ========================================================================== */
- .bl-hero { position: relative; margin: 0 38px; border-radius: 30px; overflow: hidden; padding: 170px 0 80px; }
- .bl-hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
- .bl-hero-overlay { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(6,20,49,0.82), rgba(6,20,49,0.3) 60%); }
- .bl-hero-inner { position: relative; z-index: 2; color: #fff; max-width: 640px; }
- .bl-hero-inner h1 { font-family: var(--serif); font-size: 50px; margin-bottom: 12px; }
- .bl-hero-inner p { font-weight: 500; color: rgba(255,255,255,0.85); }
-
- .bl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 26px; }
- .bl-card { padding: 22px;
- transition: box-shadow .2s, transform .2s;
- box-sizing: border-box;
- background: #FFFFFF;
- border: 1px solid #EFEFEF;
- box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
- border-radius: 20px; }
- .bl-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-3px); }
- .bl-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 46px; }
- .bl-tag { font-family: var(--display); font-size: 13px; background: #eef2f7; color: #000000; padding: 6px 16px; border-radius: 100px; }
- .bl-thumb { width: 150px; height: 104px; border-radius: 10px; overflow: hidden; }
- .bl-thumb img { width: 100%; height: 100%; object-fit: cover; }
- .bl-meta span {
- display: flex;
- align-items: center;
- gap: 10px;
- }
- .bl-meta { color: var(--ink-2);
- gap: 18px;
- margin-bottom: 42px;
- font-size: 15px;
- display: flex;
- justify-content: space-between; }
- .bl-card h3 { font-family: var(--serif); font-size: 24px; margin-bottom: 10px; }
- .bl-card p { font-size: 14px; color: var(--ink-2); margin-bottom: 18px; }
- .bl-more { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; color: var(--blue); }
-
- /* detail */
- .bd-grid { display: grid; grid-template-columns: 1fr 360px; gap: 50px; align-items: start; }
- .bd-feature { width: 100%; height: 420px; object-fit: cover; border-radius: 18px; margin-bottom: 24px; }
- .bd-meta { display: flex; gap: 22px; font-size: 14px; color: var(--ink-2); margin-bottom: 16px; }
- .bd-title { font-family: var(--serif); font-size: 40px; margin-bottom: 18px; }
- .bd-main p { font-size: 16px; color: var(--ink-2); line-height: 1.85; margin-bottom: 18px; }
- .bd-split { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: 26px; align-items: center; margin: 14px 0 30px; }
- .bd-split img { width: 100%; height: 280px; object-fit: cover; border-radius: 14px; }
- .bd-split h3 { font-family: var(--serif); font-size: 26px; margin-bottom: 16px; }
- .bd-checks { display: flex; flex-direction: column; gap: 12px; }
- .bd-checks li { font-size: 15px; color: var(--ink); }
- .bd-quote { margin: 10px 0 34px;
- padding: 60px 40px;
- position: relative;
- background: #FFFFFF;
- box-shadow: 0px 0px 60px rgba(0, 0, 0, 0.05);
- border-radius: 20px; }
- .bd-quote-mark { }
- .bd-quote p { color: var(--ink);
- margin: 38px 0 39px;
- font-size: 20px;
- line-height: 1.5; }
- .bd-quote footer strong { font-size: 22px;
- display: block; }
- .bd-quote footer span { font-size: 14px; color: var(--ink-2); }
- .bd-h2 { font-family: var(--serif); font-size: 32px; margin: 8px 0 18px; }
- .bd-tagrow { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin: 30px 0; padding: 24px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); flex-wrap: wrap; }
- .bd-tags { display: flex; flex-wrap: wrap; gap: 10px; }
- .bd-tags span { font-size: 14px; background: var(--bg-blue); color: var(--blue); padding: 7px 16px; border-radius: 100px; }
- .bd-share { display: flex; gap: 10px; }
- .bd-share a { width: 36px; height: 36px; border-radius: 50%; background: #eef2f7; display: grid; place-items: center; font-size: 13px; color: var(--ink-2); }
- .bd-comment {
- margin-top: 34px;
- background: #FFFFFF;
- border: 1px solid #EFEFEF;
- box-shadow: 5px 20px 65px rgba(0, 0, 0, 0.06);
- border-radius: 20px;
- padding: 30px 40px;
- }
- .bd-comment-note { font-size: 14px; color: var(--ink-2); margin-bottom: 20px; }
- .bd-comment-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 16px; }
- .bd-comment input, .bd-comment textarea { width: 100%; border: 1px solid var(--line); border-radius: 10px; padding: 13px 16px; font: inherit; font-size: 15px; background: #fbfcfe; }
- .bd-comment textarea { resize: vertical; margin-bottom: 14px; }
- .bd-check { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--ink-2); margin-bottom: 18px; }
- .bd-check input { width: auto; }
-
- .bd-side { display: flex; flex-direction: column; gap: 24px; position: sticky; top: 100px; }
- .bd-widget { padding: 24px;
- background: #FFFFFF;
- box-shadow: 0px 0px 60px rgba(0, 0, 0, 0.05);
- border-radius: 10px; }
- .bd-widget h4 { font-family: var(--serif);
- margin-bottom: 21px;
- font-size: 24px;
- border-bottom: 1px solid #80808029;
- padding-bottom: 30px;}
- .abt-hero {
- padding-top: 320px;
- }
- .bd-search { display: flex; gap: 8px; }
- .bd-search input { flex: 1; border: 1px solid var(--line); border-radius: 30px; padding: 12px 14px; font: inherit; background: #fff; }
- .bd-search button { position: absolute;
- color: #000000;
- border-radius: 10px;
- width: 36px;
- font-size: 26px;
- right: 34px; }
- .bd-pop { display: flex; gap: 14px; align-items: center; margin-bottom: 16px; }
- .bd-pop img { object-fit: cover;
- border-radius: 8px;
- flex-shrink: 0;
- width: 70px;
- height: 73px; }
- .bd-pop span img {
- width: 15px;
- height: 15px;
- object-fit: fill;
- border-radius: 0;
- margin-right: 6px;
-
- }
- .bd-pop span { display: flex; font-size: 12px; color: var(--ink-3); margin-bottom: 4px; }
- .bd-pop strong { font-size: 15px; font-weight: 500; }
- .bd-cat { display: block; font-size: 15px; color: black; padding: 10px 0; border-bottom: 1px solid var(--line); }
- .bd-cat:last-child { border-bottom: none; }
-
- @media (max-width: 1000px) {
- .bl-grid { grid-template-columns: 1fr 1fr; }
- .bd-grid { grid-template-columns: 1fr; }
- .bd-side { position: static; }
- }
- @media (max-width: 640px) {
- .bl-grid { grid-template-columns: 1fr; }
- .bd-split, .bd-comment-row { grid-template-columns: 1fr; }
- }
-
- /* ============================ SERVICES PAGE ============================== */
- .sv-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; margin-top: 44px; }
- .sv-card { background: #fff; border: 1px solid var(--line); border-radius: 16px; padding: 30px; transition: box-shadow 0.2s, transform 0.2s; }
- .sv-card:hover { box-shadow: var(--shadow-sm); transform: translateY(-4px); }
- .sv-icon { display: inline-grid; place-items: center; width: 52px; height: 52px; border-radius: 14px; background: var(--bg-blue); font-size: 22px; margin-bottom: 20px; }
- .sv-card h4 { font-family: var(--serif); font-size: 22px; margin-bottom: 10px; }
- .sv-card p { font-size: 14px; color: var(--ink-2); }
- @media (max-width: 1000px) { .sv-grid { grid-template-columns: 1fr 1fr; } }
- @media (max-width: 600px) { .sv-grid { grid-template-columns: 1fr; } }
-
- /* ----------------------- home responsive -------------------------------- */
- @media (max-width: 1100px) {
- .proj-big, .proj-small, .companies-grid, .testi-grid { grid-template-columns: 1fr 1fr; }
- .why-grid { grid-template-columns: 1fr; }
- .why-visual { height: 360px; }
- .journey-card { grid-template-columns: 100px 1fr; }
- .journey-middle { grid-template-columns: 1fr; }
- }
- @media (max-width: 768px) {
- .hero { height: 620px; margin: 0 12px; }
- .hero-content { padding: 0 26px; }
- .hero-content h1 { font-size: 32px; }
- .hero-note { display: none; }
- .walk-tabs, .stage-cards { grid-template-columns: 1fr 1fr; }
- .walk-stage, .virtual-stage { height: 380px; }
- .proj-big, .proj-small, .companies-grid, .testi-grid { grid-template-columns: 1fr; }
- .testi-card:nth-child(2) { margin-top: 0; }
- .dream-title { font-size: 40px; }
- .dream-inner { padding-bottom: 60px; }
- .journey-card { grid-template-columns: 1fr; }
- .journey-image { width: 100%; height: 180px; }
- .head-aside { align-items: flex-start; }
- }
-
-
\ No newline at end of file
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
index ecb8442..413b9dc 100644
--- a/src/components/Footer.tsx
+++ b/src/components/Footer.tsx
@@ -1,45 +1,47 @@
+"use client";
+
import Link from "next/link";
+import { useLang } from "@/components/i18n";
export default function Footer() {
+ const { t } = useLang();
return (
- Yamuna
-
+
{/* Brand column */}
-
+
- Yamuna Expressway is an end-to-end architecture, approvals and
- luxury villa construction practice operating across YEIDA sectors
- 18, 20, 22 and 24, Greater Noida.
+ {t(
+ "Yamuna Expressway is an end-to-end architecture, approvals and luxury villa construction practice operating across YEIDA sectors 18, 20, 22 and 24, Greater Noida."
+ )}
RERA · UPRERAAGT01188 GSTIN · 09AAACY1234L1ZQ
-
Popular
- About Us
- Services
- Portfolio
- Insights
- Contact
+ {t("Popular")}
+ {t("About Us")}
+ {t("Portfolio")}
+ {t("Insights")}
+ {t("Contact")}
-
Help
- FAQ
- Plot Portfolio
- 3D Walkthroughs
- Project Gallery
- Yamuna Growth
+ {t("Help")}
+ {t("FAQ")}
+ {t("Privacy Policy")}
+ {t("3D Walkthroughs")}
+ {t("Project Gallery")}
+ {t("Yamuna Growth")}
-
Contact
+
{t("Contact")}
+91 98106 04200
studio@yamunaexpressway.in
@@ -47,21 +49,23 @@ export default function Footer() {
Jewar Road, Greater Noida
-
Mon – Sat · 10:00 – 19:00
+
{t("Mon – Sat · 10:00 – 19:00")}
-
-
© 2026 Yamuna Expressway Authority by Insignia Consultancy Solutions. All rights reserved.
+
+
+
+
+
{t("© 2026 Yamuna Expressway Authority by Insignia Consultancy Solutions. All rights reserved.")}
-
f
-
in
-
○
-
x
-
▶
+
+
+
+
+
-
);
}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 45d1bbe..68b52e3 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -3,6 +3,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
+import { useLang, LangToggle } from "@/components/i18n";
const NAV = [
{ label: "Home", href: "/" },
@@ -15,6 +16,7 @@ const NAV = [
export default function Header() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
+ const { t } = useLang();
const isActive = (href: string) =>
href === "/" ? pathname === "/" : pathname.startsWith(href);
@@ -39,7 +41,7 @@ export default function Header() {
className={`nav-link ${isActive(n.href) ? "active" : ""}`}
onClick={() => setOpen(false)}
>
- {n.label}
+ {t(n.label)}
))}
@@ -57,7 +59,8 @@ export default function Header() {
-
Login
+
+
{t("Login")}
] = . Curated core terms; merged with the large
+// auto-generated dictionary in i18n-hi.js below. Entries here win on collision.
+const BASE_HI = {
+ // ---- Public website: header / nav ----
+ 'Home': 'होम',
+ 'About Us': 'हमारे बारे में',
+ 'Our Projects': 'हमारी परियोजनाएँ',
+ 'Insights': 'जानकारी',
+ 'Contact': 'संपर्क',
+ 'Login': 'लॉगिन',
+ 'Portfolio': 'पोर्टफोलियो',
+
+ // ---- Footer ----
+ 'Popular': 'लोकप्रिय',
+ 'Help': 'सहायता',
+ 'FAQ': 'सामान्य प्रश्न',
+ 'Privacy Policy': 'गोपनीयता नीति',
+ '3D Walkthroughs': '3D वॉकथ्रू',
+ 'Project Gallery': 'प्रोजेक्ट गैलरी',
+ 'Yamuna Growth': 'यमुना विकास',
+ 'Mon – Sat · 10:00 – 19:00': 'सोम – शनि · 10:00 – 19:00',
+ 'Yamuna Expressway is an end-to-end architecture, approvals and luxury villa construction practice operating across YEIDA sectors 18, 20, 22 and 24, Greater Noida.':
+ 'यमुना एक्सप्रेसवे एक संपूर्ण आर्किटेक्चर, स्वीकृति और लक्ज़री विला निर्माण सेवा है, जो ग्रेटर नोएडा के YEIDA सेक्टर 18, 20, 22 और 24 में कार्यरत है।',
+ '© 2026 Yamuna Expressway Authority by Insignia Consultancy Solutions. All rights reserved.':
+ '© 2026 यमुना एक्सप्रेसवे अथॉरिटी, Insignia Consultancy Solutions द्वारा। सर्वाधिकार सुरक्षित।',
+
+ // ---- Portal: sidebar nav ----
+ 'Dashboard': 'डैशबोर्ड',
+ 'My Plot': 'मेरा प्लॉट',
+ 'My Journey': 'मेरी यात्रा',
+ 'Sample Plans': 'सैंपल प्लान',
+ 'Construction': 'निर्माण',
+ 'Payments': 'भुगतान',
+ 'Documents': 'दस्तावेज़',
+ 'Notices': 'सूचनाएँ',
+ 'Sector Maps': 'सेक्टर मानचित्र',
+ 'Transfer Plot': 'प्लॉट ट्रांसफर',
+ 'AI Assistant': 'AI सहायक',
+ 'Support': 'सहायता केंद्र',
+ 'Profile': 'प्रोफ़ाइल',
+ 'Logout': 'लॉगआउट',
+
+ // ---- Portal: topbar titles ----
+ 'My Plot Dashboard': 'मेरा प्लॉट डैशबोर्ड',
+ 'Document Center': 'दस्तावेज़ केंद्र',
+ 'Notices & Announcements': 'सूचनाएँ व घोषणाएँ',
+ 'Support Center': 'सहायता केंद्र',
+ 'Sample Plans & Virtual Tours': 'सैंपल प्लान व वर्चुअल टूर',
+ 'Get Started': 'शुरू करें',
+ 'My Construction Journey': 'मेरी निर्माण यात्रा',
+
+ // ---- Portal: topbar misc ----
+ 'Search plots, dues, documents…': 'प्लॉट, बकाया, दस्तावेज़ खोजें…',
+ 'Language': 'भाषा',
+ 'Notifications': 'सूचनाएँ',
+ 'Allottee': 'आवंटी',
+ 'Representative': 'प्रतिनिधि',
+
+ // ---- Common page headings / subtitles ----
+ 'Manage your account, security and preferences': 'अपना खाता, सुरक्षा और प्राथमिकताएँ प्रबंधित करें',
+ 'Important notices, circulars and policy updates from YEIDA': 'YEIDA की महत्वपूर्ण सूचनाएँ, परिपत्र और नीति अपडेट',
+ 'Explore plotted layouts, master plans and infrastructure across YEIDA sectors': 'YEIDA सेक्टरों के प्लॉट लेआउट, मास्टर प्लान और बुनियादी ढाँचे देखें',
+ 'Raise tickets, chat with us, or browse the help center': 'टिकट बनाएँ, हमसे चैट करें, या सहायता केंद्र देखें',
+
+ // ---- Common buttons / labels ----
+ 'Edit': 'संपादित करें',
+ 'Save': 'सहेजें',
+ 'Cancel': 'रद्द करें',
+ 'Download': 'डाउनलोड',
+ 'Preview': 'पूर्वावलोकन',
+ 'Upload': 'अपलोड',
+ 'Continue': 'जारी रखें',
+ 'Back': 'वापस',
+ 'Verified': 'सत्यापित',
+ 'Pending': 'लंबित',
+ 'New Ticket': 'नया टिकट',
+ 'All': 'सभी',
+};
+
+// Full dictionary: auto-generated coverage first, curated BASE_HI overrides it.
+const HI = Object.assign({}, EXTRA_HI, BASE_HI);
+
+const DICT = { en: {}, hi: HI };
+// reverse map (hindi → english) so we can restore the page when switching back to English
+const HI_TO_EN = {};
+for (const k in HI) HI_TO_EN[HI[k]] = k;
+
+function translate(lang, s) {
+ if (lang === 'en' || s == null) return s;
+ const d = DICT[lang] || {};
+ return d[s] != null ? d[s] : s;
+}
+
+/* ---------- Automatic page-wide translation (DOM walker) ----------
+ Translates every text node + common attributes against the dictionary, so the
+ whole website & portal flip language without wrapping each string in code.
+ Idempotent: re-applying Hindi to already-Hindi text is a no-op (no lookup hit). */
+const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'CODE', 'KBD']);
+const ATTRS = ['placeholder', 'title', 'alt', 'aria-label'];
+
+function mapFor(lang) { return lang === 'hi' ? HI : HI_TO_EN; }
+
+function swap(raw, dict) {
+ if (!raw) return null;
+ const key = raw.trim();
+ if (!key) return null;
+ const tr = dict[key];
+ return tr && tr !== key ? raw.replace(key, tr) : null;
+}
+
+function translateTextNode(node, dict) {
+ const next = swap(node.nodeValue, dict);
+ if (next != null) node.nodeValue = next;
+}
+
+function translateElement(el, dict) {
+ for (const a of ATTRS) {
+ if (el.hasAttribute && el.hasAttribute(a)) {
+ const next = swap(el.getAttribute(a), dict);
+ if (next != null) el.setAttribute(a, next);
+ }
+ }
+}
+
+function translateNode(lang, node) {
+ if (typeof document === 'undefined' || !node) return;
+ const dict = mapFor(lang);
+ if (node.nodeType === 3) { translateTextNode(node, dict); return; }
+ if (node.nodeType !== 1) return;
+ if (node.nodeName && SKIP_TAGS.has(node.nodeName)) return;
+ // attributes on the root element
+ translateElement(node, dict);
+ // text nodes within
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
+ acceptNode(n) {
+ const p = n.parentNode;
+ if (!p || (p.nodeName && SKIP_TAGS.has(p.nodeName))) return NodeFilter.FILTER_REJECT;
+ return n.nodeValue && n.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
+ },
+ });
+ const texts = [];
+ while (walker.nextNode()) texts.push(walker.currentNode);
+ for (const t of texts) translateTextNode(t, dict);
+ // attributes within
+ if (node.querySelectorAll) {
+ node.querySelectorAll('[placeholder],[title],[alt],[aria-label]').forEach(el => translateElement(el, dict));
+ }
+}
+
+const LangContext = createContext({ lang: 'en', setLang: () => {}, toggle: () => {}, t: (s) => s });
+
+export function LanguageProvider({ children }) {
+ const [lang, setLangState] = useState('en');
+
+ // read persisted choice after mount (keeps SSR + first client render === 'en' → no hydration mismatch)
+ useEffect(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved === 'hi' || saved === 'en') setLangState(saved);
+ } catch {}
+ const onStorage = (e) => { if (e.key === STORAGE_KEY && (e.newValue === 'hi' || e.newValue === 'en')) setLangState(e.newValue); };
+ window.addEventListener('storage', onStorage);
+ return () => window.removeEventListener('storage', onStorage);
+ }, []);
+
+ useEffect(() => {
+ if (typeof document === 'undefined') return;
+ document.documentElement.lang = lang;
+
+ // 1) translate the whole page once for the new language
+ translateNode(lang, document.body);
+
+ // 2) keep translating content React adds/updates later (only needed for Hindi;
+ // English is the source, so newly-rendered nodes are already English)
+ if (lang !== 'hi') return;
+ const obs = new MutationObserver((muts) => {
+ for (const m of muts) {
+ if (m.type === 'characterData') {
+ translateNode('hi', m.target);
+ } else if (m.type === 'attributes' && m.target) {
+ translateElement(m.target, HI);
+ } else {
+ m.addedNodes && m.addedNodes.forEach(n => translateNode('hi', n));
+ }
+ }
+ });
+ obs.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ATTRS });
+ return () => obs.disconnect();
+ }, [lang]);
+
+ const setLang = useCallback((l) => {
+ setLangState(l);
+ try { localStorage.setItem(STORAGE_KEY, l); } catch {}
+ }, []);
+
+ const value = useMemo(() => ({
+ lang, setLang,
+ toggle: () => setLang(lang === 'en' ? 'hi' : 'en'),
+ t: (s) => translate(lang, s),
+ }), [lang, setLang]);
+
+ return {children} ;
+}
+
+export function useLang() {
+ return useContext(LangContext);
+}
+
+/* Segmented EN / हिं switch. Inactive button inherits surrounding text colour,
+ so it reads well on both dark headers and light toolbars. */
+export function LangToggle({ size = 'md', style }) {
+ const { lang, setLang } = useLang();
+ const pad = size === 'sm' ? '4px 9px' : '5px 11px';
+ const fs = size === 'sm' ? 12 : 12.5;
+ const btn = (on) => ({
+ padding: pad, fontSize: fs, fontWeight: 700, border: 'none', cursor: 'pointer',
+ borderRadius: 99, lineHeight: 1, transition: 'all .18s',
+ background: on ? 'var(--blue-600, #2670e6)' : 'transparent',
+ color: on ? '#fff' : 'inherit', opacity: on ? 1 : 0.75,
+ });
+ return (
+
+ setLang('en')} style={btn(lang === 'en')}>EN
+ setLang('hi')} style={btn(lang === 'hi')}>हिं
+
+ );
+}
+
+export default LanguageProvider;
diff --git a/src/components/portal/assistant.jsx b/src/components/portal/assistant.jsx
new file mode 100644
index 0000000..ee5988d
--- /dev/null
+++ b/src/components/portal/assistant.jsx
@@ -0,0 +1,139 @@
+'use client';
+/* AI Assistant */
+import React, { useState, useRef, useEffect } from 'react';
+import Icon from './icons';
+import YEIDA from './data';
+
+const D = YEIDA;
+
+ const ANSWERS = [
+ { match: ['construction', 'start', 'build'], reply: "To start construction on Plot B-47:\n\n1. Ensure possession is granted (✓ done for your plot)\n2. Prepare your architect-stamped building plan\n3. Upload required documents in the Construction module\n4. Submit for YEIDA scrutiny\n\nYour application is currently at Step 4 — Authority Review. Estimated decision: 12 Jun 2026.", action: ['construction', 'Open Construction'] },
+ { match: ['document', 'required', 'papers'], reply: "For building plan approval you'll need:\n\n• Approved Building Plan (architect-stamped)\n• Structural Stability Certificate\n• Architect Registration (COA)\n• Soil Test Report\n• Indemnity Bond\n• Possession Certificate copy\n\n5 of 6 are already uploaded. The Soil Test Report is still pending.", action: ['documents', 'View Documents'] },
+ { match: ['notice', 'latest', 'update'], reply: "Here are the latest notices:\n\n📌 Revised Annual Lease Rent rates FY 2026-27 (01 Jun)\n📌 Construction guidelines — FAR & setback (24 May)\n• Water supply augmentation, Sector 18 & 20 (18 May)\n\nThe lease rent revision affects your next due on 30 Jun.", action: ['notices', 'Open Notices'] },
+ { match: ['transfer', 'sell', 'ownership'], reply: "To transfer Plot B-47:\n\n1. Pass the eligibility check (5-year lock-in — ✓ you're eligible)\n2. Pay transfer charges (~₹2.02 L incl. fees & GST)\n3. Submit the transfer application with transferee KYC\n4. Authority review & approval\n\nWould you like me to open the Transfer module?", action: ['transfer', 'Start Transfer'] },
+ { match: ['payment', 'pending', 'due', 'pay'], reply: "You have 3 outstanding dues totalling ₹1,47,585:\n\n• Water Connection Charges — ₹12,400 (Overdue)\n• Annual Lease Rent 2026-27 — ₹38,625 (due 30 Jun)\n• Location Premium 14/16 — ₹96,560 (due 15 Jul)\n\nClear the overdue water charge first to avoid interest.", action: ['payments', 'Pay Dues'] },
+ { match: ['map', 'sector', '18', 'plot location'], reply: "Plot B-47 is in Sector 18, a developed residential sector along the Yamuna Expressway. It's an east-facing 300 sq.m plot on a 12 m road in Block B.\n\nI can open the interactive Sector 18 map so you can locate it.", action: ['sectormaps', 'Open Map'] },
+ ];
+ const fallback = "I can help with construction approvals, payments, documents, transfers, notices and sector maps for your plot. Try one of the suggested questions, or ask me anything about Plot B-47.\n\nIf you'd rather speak to a person, I can connect you to a YEIDA support officer.";
+ // Explicit human-handoff intent — AI is available to everyone; a human is one tap away when needed
+ const humanWords = ['human', 'agent', 'person', 'representative', 'talk to', 'speak to', 'call me', 'officer', 'someone'];
+ const handoff = { reply: "Sure — I'll connect you to a YEIDA support officer. Your chat opens in the Message Center, where a human agent takes over.", action: ['support', 'Talk to a human'] };
+
+ function reply(text) {
+ const t = text.toLowerCase();
+ if (humanWords.some(w => t.includes(w))) return handoff;
+ const hit = ANSWERS.find(a => a.match.some(m => t.includes(m)));
+ return hit || { reply: fallback, action: ['support', 'Talk to a human'] };
+ }
+
+ function Assistant({ go }) {
+ const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hello Rajeev 👋 I'm your YEIDA assistant. Ask me anything about Plot B-47 — construction, payments, documents, transfers or sector maps." }]);
+ const [input, setInput] = useState('');
+ const [typing, setTyping] = useState(false);
+ const scroller = useRef(null);
+ useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing]);
+
+ const send = (text) => {
+ if (!text.trim()) return;
+ setMsgs(m => [...m, { role: 'user', text }]);
+ setInput(''); setTyping(true);
+ setTimeout(() => {
+ const r = reply(text);
+ setTyping(false);
+ setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]);
+ }, 900);
+ };
+
+ return (
+
+ {/* chat */}
+
+
+
+
+
YEIDA AI Assistant
Online · Knowledge base connected
+
+
History
+
+
+
+ {msgs.length === 1 && (
+
+
+
Ask anything about your plot
+
Choose a question below or type your own.
+
+ {D.assistantSuggestions.map(s => (
+ send(s)} className="card-hover" style={{ textAlign: 'left', padding: '13px 15px', borderRadius: 12, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, fontSize: 13.5, fontWeight: 600, color: 'var(--ink-2)' }}>
+ {s}
+
+ ))}
+
+
+ )}
+ {msgs.map((m, i) =>
)}
+ {typing && (
+
+
+
+ {[0, 1, 2].map(i => )}
+
+
+ )}
+
+
+
+
+ setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask about construction, payments, documents…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 14.5, color: 'var(--ink)' }} />
+ send(input)}>
+
+
AI responses are informational. Verify critical actions with the Authority.
+
+
+
+ {/* side panel */}
+
+
+
Smart Suggestions
+
+ {['What payments are pending?', 'Show Sector 18 map', 'How can I transfer my plot?'].map(s => (
+ send(s)} style={{ textAlign: 'left', fontSize: 13, fontWeight: 500, color: 'var(--ink-2)', padding: '10px 12px', borderRadius: 10, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer' }} className="card-hover">{s}
+ ))}
+
+
+
+
Knowledge Sources
+
+ {[['Notices & Circulars', 'notices'], ['Document Center', 'documents'], ['Building Byelaws', 'file'], ['Sector Master Plans', 'map']].map(([l, ic]) => (
+
{l}
+ ))}
+
+
+
+
+
Need a human?
+
Escalate to a YEIDA support officer for complex queries.
+
go('support')}>Talk to an Agent
+
+
+
+ );
+ }
+
+ function Bubble({ m, go }) {
+ const isUser = m.role === 'user';
+ return (
+
+
+
+
+
+
{m.text}
+ {m.action &&
go(m.action[0])}>{m.action[1]} }
+
+
+ );
+ }
+
+export { Assistant };
+export default Assistant;
diff --git a/src/components/portal/auth.jsx b/src/components/portal/auth.jsx
new file mode 100644
index 0000000..2ed7e4e
--- /dev/null
+++ b/src/components/portal/auth.jsx
@@ -0,0 +1,372 @@
+'use client';
+/* Authentication: Welcome, Login select, shared Verification (email+phone OTP) */
+import React, { useState, useEffect, useRef } from 'react';
+import Icon from './icons';
+import { Logo, StatusBadge } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA;
+
+ /* ---- Brand panel (navy decorative side) ---- */
+ function BrandPanel({ compact }) {
+ return (
+
+
+
+
+
+
+ Official Allottee Portal
+
+
Your complete Yamuna Expressway property portal.
+
Verify your allotment, view official authority records, track construction and manage documents — securely.
+
+
+ {[['7+', 'Active Sectors'], ['62,000+', 'Allottees'], ['100%', 'Digital']].map((x, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ function AuthShell({ wide, children }) {
+ return (
+
+
+
+ {/* Scroll container (outer) + centering wrapper (inner): tall content stays
+ scrollable from the very top, short content still centers vertically. */}
+
+
+
+ );
+ }
+
+ /* ---------------- WELCOME ---------------- */
+ function Welcome({ go }) {
+ return (
+
+
+
+
+
+
+
+
+ Government of Uttar Pradesh · Official Portal
+
+
Your Complete Yamuna Expressway Property Management Portal
+
Verify your allotment, view official authority payment records, track construction approvals and manage documents — all in one secure place.
+
+ go('login')}>Login to Portal
+ go('sectormaps-public')}> Explore Sector Maps
+
+
+ SSL Secured
+ OTP Verified Access
+ DigiLocker Ready
+
+
+
+
+
+ );
+ }
+
+ function HeroCard() {
+ const plots = [[20, 18], [64, 18], [108, 18], [186, 18], [230, 18], [274, 18], [20, 116], [64, 116], [186, 116], [230, 116], [274, 116]];
+ return (
+
+
+
+
+
+
+
+
Plot B-47, Sector 18
+
Residential · 300 sq.m
+
+
+
+
+
+
+
+
+
+
+
+ {plots.map((p, i) => )}
+
+
+ B-47
+
+
+
+
+ );
+ }
+
+ /* ---------------- LOGIN SELECTION ---------------- */
+ function LoginSelect({ go }) {
+ const [mode, setMode] = useState('allotment');
+ // Either an allotment number OR a plot number may be used — only one is required.
+ const [allotment, setAllotment] = useState('YEA-654321');
+ const [invite, setInvite] = useState('');
+ const [plot, setPlot] = useState({ sector: D.sectors[0].name, pocket: 'Pocket B', plot: '' });
+ const [plotState, setPlotState] = useState('idle'); // idle | checking | verified | notfound
+ const opts = [
+ { k: 'allotment', icon: 'building', title: 'Allotment Number', desc: 'Login with your YEIDA allotment number' },
+ { k: 'plot', icon: 'plot', title: 'Plot Number', desc: 'No allotment number? Use your sector, pocket & plot' },
+ { k: 'invite', icon: 'key', title: 'Invitation Code', desc: 'Access via authority-issued invite code' },
+ ];
+
+ const setPlotField = (kk, vv) => { setPlot(p => ({ ...p, [kk]: vv })); setPlotState('idle'); };
+ const verifyPlot = () => {
+ if (!plot.plot.trim()) return;
+ setPlotState('checking');
+ setTimeout(() => setPlotState('verified'), 800); // demo lookup → resolves to an allotment
+ };
+
+ // Continue gate: allotment filled, OR plot verified, OR invite filled.
+ const canContinue = mode === 'allotment' ? allotment.trim().length > 0
+ : mode === 'plot' ? plotState === 'verified'
+ : invite.trim().length > 0;
+
+ return (
+
+
+
Welcome back
+
Sign in to your portal
+
Verify yourself using your allotment number, your plot number, or an invitation code.
+
+ {opts.map(o => (
+
setMode(o.k)} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '15px 16px', borderRadius: 14, textAlign: 'left', border: '1.5px solid ' + (mode === o.k ? 'var(--blue-400)' : 'var(--line)'), background: mode === o.k ? 'var(--blue-50)' : '#fff', boxShadow: mode === o.k ? '0 0 0 4px rgba(38,112,230,.1)' : 'none', transition: 'all .15s', cursor: 'pointer' }}>
+
+
+ {mode === o.k && }
+
+ ))}
+
+
+ {mode === 'allotment' && (
+
+
Allotment Number
+
setAllotment(e.target.value.toUpperCase())} />
+
setMode('plot')} style={{ alignSelf: 'flex-start', background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 600, fontSize: 12.5, cursor: 'pointer', padding: 0, marginTop: 2 }}>I don't remember my allotment number — use plot number
+
+ )}
+
+ {mode === 'invite' && (
+
+
Invitation Code
+
setInvite(e.target.value)} />
+
+ )}
+
+ {mode === 'plot' && (
+
+
+
Sector setPlotField('sector', e.target.value)}>{D.sectors.map(s => {s.name} )}
+
Pocket setPlotField('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => {p} )}
+
+
+
Plot Number
+
+
setPlotField('plot', e.target.value)} placeholder="e.g. B-47 or 047" />
+ {plotState === 'verified' && }
+
+ {plotState !== 'verified' &&
+ {plotState === 'checking' ? : }Verify
+ }
+
+ {plotState === 'verified'
+ ?
Plot verified · matched to allotment YEA-654321
+ : plotState === 'notfound'
+ ?
Plot not found — check sector & pocket, or use your allotment number
+ :
Unverified — verify your plot to continue (no allotment number needed). }
+
+
+ )}
+
+
go('verify')}>Continue
+
+ New allottee or representative?{' '}
+ go('register')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 14 }}>Register here
+
+
+
+ );
+ }
+
+ /* ---------------- SHARED VERIFICATION (email + phone, same screen) ---------------- */
+ // VerifyPanel renders both channels and reports completion via onChange(bothVerified)
+ function VerifyPanel({ email, phone, onChange }) {
+ const [cc, setCc] = useState(D.countryCodes[0]);
+ const [state, setState] = useState({ email: false, phone: false });
+ return (
+
+ setState(s => { const n = { ...s, email: v }; onChange && onChange(n.email && n.phone); return n; })} />
+ setState(s => { const n = { ...s, phone: v }; onChange && onChange(n.email && n.phone); return n; })} />
+
+ );
+ }
+
+ // channel that lifts verified state up to the panel
+ function VerifyChannelLifted({ kind, value, cc, setCc, onVerified }) {
+ const [sent, setSent] = useState(false);
+ const [otp, setOtp] = useState(['', '', '', '', '', '']);
+ const [verified, setVerified] = useState(false);
+ const [cooldown, setCooldown] = useState(0);
+ const [checking, setChecking] = useState(false);
+ const [api, setApi] = useState('idle'); // idle|checking|ok|invalid_email|mailbox_full|bounce_risk|invalid_mobile
+ const [otpErr, setOtpErr] = useState(false);
+ const [field, setField] = useState(value || '');
+ const refs = useRef([]);
+ useEffect(() => { if (cooldown <= 0) return; const t = setInterval(() => setCooldown(s => s - 1), 1000); return () => clearInterval(t); }, [cooldown]);
+ useEffect(() => { onVerified && onVerified(verified); }, [verified]);
+
+ const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(field || '');
+ const digits = (field || '').replace(/\D/g, '').length;
+ const phoneOk = digits >= (cc ? cc.digits : 10);
+ const valid = kind === 'email' ? emailOk : phoneOk;
+
+ // simulate an email/phone validation API — keywords in the value trigger states (demo)
+ const runApi = () => {
+ setApi('checking'); setChecking(true);
+ setTimeout(() => {
+ setChecking(false);
+ const v = (field || '').toLowerCase();
+ if (kind === 'email') {
+ if (!emailOk) return setApi('invalid_email');
+ if (v.includes('full')) return setApi('mailbox_full');
+ if (v.includes('bounce') || v.includes('temp')) return setApi('bounce_risk');
+ setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50);
+ } else {
+ if (!phoneOk) return setApi('invalid_mobile');
+ setApi('ok'); setSent(true); setCooldown(60); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50);
+ }
+ }, 950);
+ };
+
+ const API_MSG = {
+ checking: { tone: 'blue', icon: 'clock', text: kind === 'email' ? 'Validating email via API…' : 'Validating mobile via API…' },
+ ok: { tone: 'green', icon: 'checkCircle', text: kind === 'email' ? 'Email deliverable · OTP sent' : 'Mobile valid · OTP sent' },
+ invalid_email: { tone: 'red', icon: 'alert', text: 'Invalid email address' },
+ mailbox_full: { tone: 'amber', icon: 'alert', text: 'Mailbox full — try another email' },
+ bounce_risk: { tone: 'amber', icon: 'alert', text: 'High bounce risk — verify spelling' },
+ invalid_mobile: { tone: 'red', icon: 'alert', text: `Invalid mobile number for ${cc ? cc.label : 'IN'}` },
+ };
+
+ const set = (i, v) => {
+ if (!/^\d?$/.test(v)) return;
+ const d = otp.slice(); d[i] = v; setOtp(d); setOtpErr(false);
+ if (v && i < 5) refs.current[i + 1] && refs.current[i + 1].focus();
+ if (d.every(x => x !== '')) {
+ // demo: "000000" simulates an incorrect OTP
+ if (d.join('') === '000000') { setOtpErr(true); setOtp(['', '', '', '', '', '']); setTimeout(() => refs.current[0] && refs.current[0].focus(), 50); }
+ else setVerified(true);
+ }
+ };
+ const onKey = (i, e) => { if (e.key === 'Backspace' && !otp[i] && i > 0) refs.current[i - 1] && refs.current[i - 1].focus(); };
+
+ const apiMsg = API_MSG[api];
+ const toneColor = { red: 'var(--red-600)', amber: 'var(--amber-600)', blue: 'var(--blue-600)', green: 'var(--emerald-700)' };
+ const toneBg = { red: 'var(--red-50)', amber: 'var(--amber-50)', blue: 'var(--blue-50)', green: 'var(--emerald-50)' };
+
+ return (
+
+
+
+
+
+
{kind === 'email' ? 'Verify Email' : 'Verify Phone Number'}
+
{verified ? 'Verified successfully' : sent ? 'Enter the 6-digit OTP' : 'OTP verification required'}
+
+
+ {verified &&
Verified}
+
+ {kind === 'email' ? (
+
{ setField(e.target.value); setApi('idle'); }} disabled={sent} />
+ ) : (
+
+
setCc(D.countryCodes.find(c => c.code === e.target.value))} disabled={sent} style={{ width: 100, flex: 'none' }}>
+ {D.countryCodes.map(c => {c.label} {c.code} )}
+
+
{ setField(e.target.value); setApi('idle'); }} disabled={sent} />
+
+ )}
+
+ {/* API validation status banner */}
+ {apiMsg && (
+
+ {api === 'checking' ? : }
+ {apiMsg.text}
+
+ )}
+
+ {!sent ? (
+
+ {checking ? <> {kind === 'email' ? 'Checking email…' : 'Sending OTP…'}> : <> Send OTP>}
+
+ ) : (
+
+
+ {otp.map((d, i) => (
+ refs.current[i] = el} value={d} inputMode="numeric" maxLength={1} disabled={verified}
+ onChange={e => set(i, e.target.value)} onKeyDown={e => onKey(i, e)}
+ style={{ width: 44, height: 50, textAlign: 'center', fontSize: 20, fontWeight: 700, fontFamily: 'var(--font-display)', border: '1.5px solid ' + (otpErr ? 'var(--red-500)' : d ? (verified ? '#7fd3ad' : 'var(--blue-400)') : 'var(--line)'), borderRadius: 11, color: 'var(--ink)', outline: 'none', background: otpErr ? 'var(--red-50)' : d ? (verified ? '#fff' : 'var(--blue-50)') : '#fff' }} />
+ ))}
+
+ {otpErr &&
OTP incorrect — please try again
}
+ {!verified && (
+
+
Didn't get the code?
+
+ {kind === 'email' && cooldown <= 0 && setCooldown(60)} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 12.5 }}>Resend Email }
+ {cooldown > 0
+ ? Resend in 0:{String(cooldown).padStart(2, '0')}
+ : setCooldown(60)} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 12.5 }}>Resend OTP }
+
+
+ )}
+
+ )}
+
+ );
+ }
+
+ /* standalone Verify screen (login path) */
+ function Verify({ go }) {
+ const [both, setBoth] = useState(false);
+ return (
+
+
+
go('login')}> Back
+
+
Verify it's you
+
Confirm both your email and mobile to securely access your records.
+
+
go('app')}>{both ? 'Continue to Portal' : 'Verify email & phone to continue'}
+
Demo: enter any 6 digits to verify · type "000000" for an incorrect-OTP state · use an email with "full"/"bounce" to see API states
+
+
+ );
+ }
+
+export { Welcome, LoginSelect, Verify, AuthShell, VerifyPanel, BrandPanel };
diff --git a/src/components/portal/construction.jsx b/src/components/portal/construction.jsx
new file mode 100644
index 0000000..4df844c
--- /dev/null
+++ b/src/components/portal/construction.jsx
@@ -0,0 +1,221 @@
+'use client';
+/* Construction module — 6-step stepper */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { PageHead } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA;
+
+ function Construction({ go, toast }) {
+ const steps = D.constructionSteps;
+ const [active, setActive] = useState(3); // Authority Review
+ const [docs, setDocs] = useState(D.requiredDocs);
+
+ const upload = (i) => { setDocs(d => d.map((x, j) => j === i ? { ...x, status: 'uploaded' } : x)); toast && toast('Document uploaded for scrutiny'); };
+
+ return (
+
+
+ go('documents')}> Building Docs
+
+
+ {/* Stepper rail */}
+
+
+ {steps.map((s, i) => {
+ const state = s.done ? 'done' : i === active ? 'active' : 'todo';
+ const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : 'var(--line)';
+ return (
+
+ {i < steps.length - 1 &&
}
+
setActive(i)} style={{ position: 'relative', zIndex: 1, background: 'none', border: 'none', cursor: 'pointer', width: '100%', textAlign: 'center', padding: 0 }}>
+
+ {s.done ? : {i + 1} }
+
+ {s.title}
+ {s.desc}
+
+
+ );
+ })}
+
+
+
+ {/* Step content */}
+
+ {active === 0 &&
}
+ {active === 1 &&
}
+ {active === 2 &&
}
+ {active === 3 &&
}
+ {active === 4 &&
}
+ {active === 5 &&
}
+
+
+ {/* nav */}
+
+ setActive(a => Math.max(0, a - 1))}> Previous
+ setActive(a => Math.min(5, a + 1))}>Next Step
+
+
+ );
+ }
+
+ function Card({ children, ...p }) { return {children}
; }
+
+ function Overview({ go }) {
+ return (
+
+
+ Step 1 · Overview
+ Start construction on Plot B-47
+ Before beginning construction, your building plan must be sanctioned by YEIDA in accordance with the prevailing building byelaws, FAR and setback norms for {D.user.sector}.
+
+ {[['Max Ground Coverage', '50%'], ['Permissible FAR', '1.5'], ['Max Height', '15 m'], ['Front Setback', '3 m']].map(([l, v]) => (
+
+ ))}
+
+
+
+ Eligibility
+ {[['Possession granted', true], ['100% premium not mandatory', true], ['No pending dues for plan', false], ['Architect empanelled', true]].map(([l, ok], i) => (
+
+
+ {l}
+
+ ))}
+
+
+ Clear the Water Connection due of ₹12,400 before plan submission.
+
+
+
+ );
+ }
+
+ function Required({ docs }) {
+ return (
+
+ Step 2 · Required Documents
+ Document checklist
+ Prepare the following before submitting your building plan for scrutiny.
+
+ {docs.map((d, i) => (
+
+
+
+
{d.status === 'uploaded' ? 'Ready' : 'Pending'}
+
+ ))}
+
+
+ );
+ }
+
+ function Upload({ docs, upload }) {
+ return (
+
+
+ Step 3 · Upload
+ Upload documents
+
+
+
Drag & drop files here
+
PDF, DWG, JPG · up to 25 MB each
+
Browse Files
+
+
+
+ Submission status
+
+ {docs.map((d, i) => (
+
+
+ {d.status === 'uploaded'
+ ?
Uploaded
+ :
upload(i)}> Upload }
+
+ ))}
+
+
+
+ );
+ }
+
+ function Review({ setActive }) {
+ return (
+
+
+
+
+
+ Step 4 · Authority Review
+ Your application is under scrutiny
+ YEIDA's Building Plan section is reviewing your submission against byelaws. You'll be notified once the sanction letter is issued.
+
+ {[['Submitted', '16 Feb 2026'], ['Application No.', 'BP-2026-18-0472'], ['Est. Decision', '12 Jun 2026'], ['Reviewing Officer', 'AE (Planning)']].map(([l, v]) => (
+
+ ))}
+
+
+
Document Check Technical Scrutiny Sanction
+
+
+
+ );
+ }
+
+ function Approval() {
+ return (
+
+
+ Step 5 · Approval
+ Sanction letter ready
+ Once approved, your building plan sanction letter will be available to download here, valid for 3 years from the date of issue.
+
+ Download Sanction Letter
+ Preview
+
+
+ );
+ }
+
+ function Progress() {
+ return (
+
+
+
Step 6 · Progress Tracker
On-site construction progress
+
+
+
+ {D.constructionMilestones.map((m, i) => (
+
+
+
+
0 ? 'var(--blue-50)' : 'var(--bg-2)', color: m.pct === 100 ? 'var(--emerald-600)' : m.pct > 0 ? 'var(--blue-600)' : 'var(--faint)' }}>
+
+
+
+ 0 ? 'blue' : 'gray')} style={{ fontSize: 11 }}>{m.status}
+ {m.pct}%
+
+
+
+
+ ))}
+
+
+ );
+ }
+
+export { Construction };
+export default Construction;
diff --git a/src/components/portal/dashboard.jsx b/src/components/portal/dashboard.jsx
new file mode 100644
index 0000000..5bc4ca7
--- /dev/null
+++ b/src/components/portal/dashboard.jsx
@@ -0,0 +1,410 @@
+'use client';
+/* Dashboard — split into A) Authority Status & Records and B) Construction Progress */
+import React from 'react';
+import Icon from './icons';
+import { Donut, StatusBadge } from './ui';
+import { useUser } from './user-store';
+import YEIDA from './data';
+
+const D = YEIDA, fmt = D.fmtINR;
+
+ function SectionLabel({ n, title, sub, children }) {
+ return (
+
+
+
Module {n}
+
{title}
+ {sub &&
{sub}
}
+
+ {children}
+
+ );
+ }
+
+ function StatusCard({ icon, title, code, date, color }) {
+ const s = D.STATUS_CODES[code] || {};
+ const tone = s.tone || 'gray';
+ const tc = { green: ['var(--emerald-50)', 'var(--emerald-700)', 'var(--emerald-500)'], amber: ['var(--amber-50)', 'var(--amber-600)', 'var(--amber-500)'], blue: ['var(--blue-50)', 'var(--blue-600)', 'var(--blue-500)'], gray: ['var(--bg-2)', 'var(--muted)', '#c2cedd'] }[tone];
+ return (
+
+
+
+ {tone === 'green' ? : tone === 'amber' ? : }
+
+
+
{title}
+
{s.label}
+
{date}
+
+
+ );
+ }
+
+ function Dashboard({ go }) {
+ const [view, setView] = React.useState(() => (typeof window !== 'undefined' ? localStorage.getItem('yeida_dash') : null) || 'plot');
+ const A = D.authorityStatus;
+ const user = useUser(D.user);
+ const switchTo = (v) => { setView(v); if (typeof window !== 'undefined') localStorage.setItem('yeida_dash', v); };
+
+ return (
+
+ {/* Welcome banner */}
+
+
+ {/* decorative concentric rings */}
+
+
+
+
+
+
+
+
Official Allottee Portal
+
Welcome back, {user.firstName}
+
+ Plot {D.user.plot} · {D.user.sector}
+ {user.relationship} · {user.isAllottee ? 'Allottee' : 'Representative'}
+ {A.leaseDeed.label}
+ {A.atl.label}
+
+
+
+ go('payments')}> Pay Dues
+ go('journey')}> What's next?
+
+
+
+
+ {/* ===== Module switcher: Plot Dashboard (primary) · Construction Project Dashboard (secondary) ===== */}
+
+ {[
+ { k: 'plot', icon: 'building', title: 'Yamuna Expressway · Plot Dashboard', sub: 'Authority status, records & payments', tag: 'Primary' },
+ { k: 'construction', icon: 'construction', title: 'Construction Project Dashboard', sub: 'Build progress, milestones & contractor', tag: D.journey.constructionStarted ? 'Active' : 'Not started' },
+ ].map(m => {
+ const on = view === m.k;
+ return (
+
switchTo(m.k)} className="card mod-switch" style={{ flex: 1, minWidth: 280, textAlign: 'left', cursor: 'pointer', padding: '16px 18px', display: 'flex', alignItems: 'center', gap: 14, border: on ? '1.5px solid var(--blue-400)' : '1px solid var(--line)', background: on ? 'linear-gradient(100deg, var(--blue-50), #fff)' : '#fff', boxShadow: on ? '0 0 0 4px rgba(38,112,230,.1)' : 'var(--sh-sm)' }}>
+
+
+
+ {m.title}
+ {m.tag}
+
+
{m.sub}
+
+ {on && }
+
+ );
+ })}
+
+
+ {view === 'plot' ?
:
}
+
+ );
+ }
+
+ /* ============================ MODULE 1 — PLOT DASHBOARD (PRIMARY) ============================ */
+ function PlotDashboard({ go }) {
+ const totalDue = D.dues.reduce((s, d) => s + d.amount, 0);
+ const paidPct = Math.round(D.property.paid / D.property.totalValue * 100);
+ const A = D.authorityStatus;
+ return (
+
+ {/* Pending actions strip */}
+
+
+
+
3 pending actions need your attention
+
ATL pending · 1 overdue payment · Soil Test Report awaited
+
+
+ go('payments')}>Clear dues
+ go('construction')}>Resolve
+
+
+
+
+ go('payments')}>View all records
+
+
+
+
+
+
+
+
+
+ {/* Authority Status — key metrics */}
+ {(() => {
+ const last = D.authorityRecords[D.authorityRecords.length - 1] || D.authorityRecords[0];
+ const nextDue = [...D.dues].sort((a, b) => a.due.localeCompare(b.due))[0] || D.dues[0];
+ const metrics = [
+ { label: 'Total Paid', value: fmt(D.property.paid), sub: D.authorityRecords.length + ' receipts', icon: 'checkCircle', color: 'emerald' },
+ { label: 'Outstanding Dues', value: fmt(totalDue), sub: D.dues.length + ' pending', icon: 'rupee', color: 'amber' },
+ { label: 'Last Payment', value: fmt(last.amount), sub: last.date, icon: 'history', color: 'blue' },
+ { label: 'Next Due', value: nextDue.due, sub: fmt(nextDue.amount) + ' · ' + nextDue.type, icon: 'calendar', color: 'navy' },
+ ];
+ const C = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'] };
+ return (
+
+ {metrics.map(m => {
+ const cc = C[m.color];
+ return (
+
+
+
+
{m.label}
+
{m.value}
+
{m.sub}
+
+
+ );
+ })}
+
+ );
+ })()}
+
+
+ {/* Authority payments summary */}
+
+
+
Authority Payments
Premium & lease account
+
go('payments')}>Details
+
+
+
Total Premium
{fmt(D.property.totalValue)}
+
Deposited
{fmt(D.property.paid)}
+
Balance
{fmt(D.property.totalValue - D.property.paid)}
+
+
+
{paidPct}% of premium deposited {D.authorityRecords.length} official receipts
+
+
+ {/* Pending dues + due dates */}
+
+
+
Pending Dues & Due Dates
+ {D.dues.length} pending
+
+
+ {D.dues.map(d => (
+
+
+
{d.label}
+
+ {d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'}
+ {d.due}
+
+
+
{fmt(d.amount)}
+
+ ))}
+
+
go('payments')}>Pay {fmt(totalDue)}
+
+
+
+ {/* Official records table */}
+
+
+
Official Authority Records Verified deposits & challans on record with YEIDA
+
go('payments')}> Statement
+
+
+ Head of Account Challan No. Date Mode Status Amount
+
+ {D.authorityRecords.map(r => (
+
+ {r.head}
+ {r.challan}
+ {r.date}
+ {r.mode}
+ {r.status}
+ {fmt(r.amount)}
+
+ ))}
+
+
+
+
+ {/* Latest Notices · AI Assistant · Recent Documents */}
+
+
+
+
Latest Notices
+ go('notices')}>All
+
+
+ {D.notices.slice(0, 3).map(n => (
+
go('notices')} style={{ textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: '8px 0', borderBottom: '1px solid var(--line-2)', display: 'flex', gap: 11 }}>
+
+
+
{n.title}
+
{n.cat} · {n.date}
+
+
+ ))}
+
+
+
+
+
+
+
AI Assistant Online · ask anything
+
+
+ {['What payments are pending?', 'What is my ATL status?'].map(q => (
+ go('assistant')} style={{ textAlign: 'left', fontSize: 12.5, fontWeight: 500, color: '#dce8f8', padding: '9px 12px', borderRadius: 10, border: '1px solid rgba(255,255,255,.14)', background: 'rgba(255,255,255,.06)', cursor: 'pointer' }}>{q}
+ ))}
+
+
go('assistant')}> Open Assistant
+
+
+
+
+
Recent Documents
+ go('documents')}>All
+
+
+ {D.documents.slice(0, 4).map((d, i) => (
+ go('documents')} className="row spread" style={{ width: '100%', textAlign: 'left', background: 'none', border: 'none', cursor: 'pointer', padding: '8px 0', borderBottom: '1px solid var(--line-2)' }}>
+
+
+ {d.name} {d.date}
+
+
+
+ ))}
+
+
+
+
+ );
+ }
+
+ /* ============================ MODULE 2 — CONSTRUCTION PROJECT DASHBOARD (SECONDARY) ============================ */
+ function ConstructionDashboard({ go }) {
+ const J = D.journey;
+ return (
+
+
+ go('construction')}>Open module
+
+
+ {/* construction financial / site widgets */}
+
+
+
+
+ {Math.round(D.construction.spent / D.construction.estimatedCost * 100)}% spent
+
+
Estimated Build Cost
+
{fmt(D.construction.estimatedCost)}
+
+
{fmt(D.construction.spent)} spent to date
+
+
+
+
+ {D.construction.contractor.status}
+
+
Contractor Status
+
{D.construction.contractor.name}
+
+ {D.construction.contractor.rating}
+ Engaged since {D.construction.contractor.since}
+
+
+
+
+
+ {D.construction.onSchedule ? 'On schedule' : 'Delayed'}
+
+
Completion Forecast
+
{D.construction.completionForecast}
+
Current: {D.construction.currentMilestone}
+
+
+
+
+ {/* status + progress */}
+
+
+ {J.constructionStarted ? 'Construction Started' : 'Not Started'}
+
+
+
+
Current Stage
+
{J.stage}
+
+
+
Next Required Action
+
+ {J.nextAction}
+ go('construction')}>
+
+
+
+
+ {/* milestones */}
+
+
Milestones
+
+ {D.constructionMilestones.map((m, i) => (
+
+
+ {m.label}
+ 0 ? 'blue' : 'gray')} style={{ fontSize: 10.5 }}>{m.status}
+
+
+
{m.date}
+
+ ))}
+
+
+
+ {/* timeline */}
+
+
Activity Timeline
+
+ {D.activity.slice(0, 4).map((a, i, arr) => {
+ const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)', navy: 'var(--navy-700)' }[a.color];
+ return (
+
+ {i !== arr.length - 1 &&
}
+
+
+
{a.title}
+
{a.desc}
+
{a.time}
+
+
+ );
+ })}
+
+
+
+
+ {/* construction payments shortcut + sample plans CTA */}
+
+
+
+
+
Construction Payments
Milestone invoices & receipts
+
+
go('payments')}>Open
+
+
+
+
+
Sample Plans & Tours
Designs matched to your plot
+
+
go('sampleplans')}>Open
+
+
+
+ );
+ }
+
+export { Dashboard };
+export default Dashboard;
diff --git a/src/components/portal/data.js b/src/components/portal/data.js
new file mode 100644
index 0000000..f318921
--- /dev/null
+++ b/src/components/portal/data.js
@@ -0,0 +1,517 @@
+/* Mock data for the YEIDA portal — realistic Yamuna Expressway style. */
+const YEIDA = (function () {
+ const fmtINR = (n) => '₹' + n.toLocaleString('en-IN');
+ const user = {
+ name: 'Rajeev Malhotra',
+ firstName: 'Rajeev',
+ lastName: 'Malhotra',
+ initials: 'RM',
+ allotment: 'YEA-654321',
+ sector: 'Sector 18',
+ pocket: 'Pocket B',
+ plot: 'B-47',
+ category: 'Residential',
+ size: '300 sq.m',
+ mobile: '+91 98xxxxxx12',
+ email: 'r.malhotra@email.com',
+ memberSince: 'Mar 2019',
+ relationship: 'Self', // Self | Son | Daughter | Spouse | Relative | Authorized Representative
+ isAllottee: true, // true = self registration (Allottee = User)
+ allotteeFirstName: 'Rajeev', // original allottee (may differ from registrant)
+ allotteeLastName: 'Malhotra',
+ registeredAddress: {
+ line1: 'Plot B-47, Pocket B, Sector 18',
+ line2: 'Yamuna Expressway, Gautam Buddh Nagar',
+ city: 'Greater Noida', state: 'Uttar Pradesh', pin: '203201',
+ },
+ mailingAddress: {
+ line1: 'C-204, Orchid Residency, Sector 50',
+ line2: 'Near City Centre',
+ city: 'Noida', state: 'Uttar Pradesh', pin: '201301',
+ },
+ };
+
+ // Real authority statuses (NOT a generic "Verified") — driven by backend status codes
+ const authorityStatus = {
+ leaseDeed: { code: 'LD_COMPLETED', label: 'Lease Deed Completed', state: 'done', date: '05 Sep 2019' },
+ atl: { code: 'ATL_PENDING', label: 'ATL Pending', state: 'pending', date: 'Action required' },
+ verification: { code: 'VERIFIED_AUTH', label: 'Authority Verified', state: 'done', date: '12 Mar 2019' },
+ possession: { code: 'POSS_GRANTED', label: 'Possession Granted', state: 'done', date: '22 Aug 2019' },
+ };
+
+ // status-code → human label + colour (single source of truth for badges)
+ const STATUS_CODES = {
+ LD_COMPLETED: { label: 'Lease Deed Completed', tone: 'green', icon: 'file' },
+ LD_PENDING: { label: 'Lease Deed Pending', tone: 'amber', icon: 'file' },
+ ATL_COMPLETED: { label: 'ATL Completed', tone: 'green', icon: 'checkCircle' },
+ ATL_PENDING: { label: 'ATL Pending', tone: 'amber', icon: 'clock' },
+ VERIFICATION_REVIEW: { label: 'Verification Under Review', tone: 'blue', icon: 'clock' },
+ VERIFIED_AUTH: { label: 'Authority Verified', tone: 'green', icon: 'shield' },
+ POSS_GRANTED: { label: 'Possession Granted', tone: 'green', icon: 'home' },
+ };
+
+ const property = {
+ allotmentNo: 'YEA-654321',
+ sector: 'Sector 18',
+ plot: 'B-47',
+ block: 'Block B',
+ size: '300 sq.m',
+ category: 'Residential',
+ facing: 'East',
+ road: '12 m road',
+ allottedOn: '14 Mar 2019',
+ possession: '22 Aug 2019',
+ leasePeriod: '90 years',
+ rate: '₹25,750 / sq.m',
+ totalValue: 7725000,
+ paid: 6952500,
+ coords: { col: 3, row: 2 },
+ };
+
+ const dues = [
+ { id: 'D-2026-Q1', label: 'Annual Lease Rent 2026-27', due: '30 Jun 2026', amount: 38625, status: 'due', type: 'Lease Rent' },
+ { id: 'D-2026-LP', label: 'Location Premium Instalment 14/16', due: '15 Jul 2026', amount: 96560, status: 'upcoming', type: 'Premium' },
+ { id: 'D-2026-WC', label: 'Water Connection Charges', due: '10 Jun 2026', amount: 12400, status: 'overdue', type: 'Utility' },
+ ];
+
+ const payments = [
+ { id: 'TXN-849213', date: '12 Mar 2026', label: 'Location Premium Instalment 13/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2026-0312-18' },
+ { id: 'TXN-831077', date: '28 Jan 2026', label: 'Annual Lease Rent 2025-26', mode: 'UPI', amount: 36750, status: 'Success', challan: 'CH-2026-0128-04' },
+ { id: 'TXN-802461', date: '15 Nov 2025', label: 'Building Plan Scrutiny Fee', mode: 'Debit Card', amount: 8500, status: 'Success', challan: 'CH-2025-1115-77' },
+ { id: 'TXN-778190', date: '12 Sep 2025', label: 'Location Premium Instalment 12/16', mode: 'Net Banking', amount: 96560, status: 'Success', challan: 'CH-2025-0912-21' },
+ { id: 'TXN-751002', date: '30 Jun 2025', label: 'Annual Lease Rent 2024-25', mode: 'UPI', amount: 35000, status: 'Success', challan: 'CH-2025-0630-09' },
+ ];
+
+ const constructionSteps = [
+ { key: 'overview', title: 'Overview', desc: 'Project & eligibility', done: true },
+ { key: 'required', title: 'Required Documents', desc: 'Checklist', done: true },
+ { key: 'upload', title: 'Upload Documents', desc: 'Building plans & approvals', done: true },
+ { key: 'review', title: 'Authority Review', desc: 'YEIDA scrutiny', done: false, active: true },
+ { key: 'approval', title: 'Approval', desc: 'Sanction letter', done: false },
+ { key: 'progress', title: 'Progress Tracker', desc: 'On-site milestones', done: false },
+ ];
+
+ const requiredDocs = [
+ { name: 'Approved Building Plan', note: 'Architect-stamped, 4 sets', status: 'uploaded' },
+ { name: 'Structural Stability Certificate', note: 'By registered structural engineer', status: 'uploaded' },
+ { name: 'Architect Registration (COA)', note: 'Valid council certificate', status: 'uploaded' },
+ { name: 'Soil Test Report', note: 'Geotechnical investigation', status: 'pending' },
+ { name: 'Indemnity Bond', note: '₹100 stamp paper, notarised', status: 'uploaded' },
+ { name: 'Possession Certificate Copy', note: 'Self-attested', status: 'uploaded' },
+ ];
+
+ const constructionMilestones = [
+ { label: 'Foundation & Plinth', pct: 100, status: 'Completed', date: 'Done · Apr 2026' },
+ { label: 'Ground Floor Structure', pct: 65, status: 'In Progress', date: 'Est. Aug 2026' },
+ { label: 'First Floor Structure', pct: 0, status: 'Not Started', date: 'Est. Nov 2026' },
+ { label: 'Finishing & Handover', pct: 0, status: 'Not Started', date: 'Est. Mar 2027' },
+ ];
+
+ const documents = [
+ { name: 'Allotment Letter', type: 'Allotment', date: '14 Mar 2019', size: '1.2 MB', tag: 'blue' },
+ { name: 'Possession Letter', type: 'Possession', date: '22 Aug 2019', size: '0.9 MB', tag: 'green' },
+ { name: 'Lease Deed (Registered)', type: 'Lease Deed', date: '05 Sep 2019', size: '3.4 MB', tag: 'navy' },
+ { name: 'Building Plan Sanction', type: 'Building Approvals', date: '18 Feb 2026', size: '5.1 MB', tag: 'amber' },
+ { name: 'Fire NOC', type: 'NOCs', date: '02 Mar 2026', size: '0.6 MB', tag: 'green' },
+ { name: 'Sector 18 Master Plan', type: 'Master Plans', date: '11 Jan 2024', size: '8.7 MB', tag: 'navy' },
+ { name: 'Circular: Transfer Policy 2026', type: 'Circulars', date: '20 Apr 2026', size: '0.4 MB', tag: 'blue' },
+ { name: 'Govt. Order — Lease Rent Revision', type: 'Government Orders', date: '01 Apr 2026', size: '0.3 MB', tag: 'amber' },
+ ];
+
+ const notices = [
+ { id: 1, title: 'Revised Annual Lease Rent rates effective FY 2026-27', cat: 'Policy Update', date: '01 Jun 2026', pinned: true, body: 'The Authority has revised annual lease rent rates for all residential and institutional allottees with effect from 1 April 2026. Allottees are advised to clear outstanding dues at the revised rate before 30 June 2026 to avoid interest.', attachments: ['GO-LeaseRent-2026.pdf'] },
+ { id: 2, title: 'Construction guidelines update — setback & FAR norms', cat: 'Construction Guidelines', date: '24 May 2026', pinned: true, body: 'Updated Floor Area Ratio (FAR) and setback norms apply to building plans submitted after 1 May 2026. Refer to the attached circular for sector-wise ground coverage limits.', attachments: ['FAR-Norms-2026.pdf', 'Setback-Chart.pdf'] },
+ { id: 3, title: 'Water supply augmentation work in Sector 18 & 20', cat: 'Sector Update', date: '18 May 2026', pinned: false, body: 'Scheduled maintenance and pipeline augmentation will be carried out from 25–28 May. Intermittent supply disruption is expected.', attachments: [] },
+ { id: 4, title: 'Online possession application window now open', cat: 'Announcement', date: '10 May 2026', pinned: false, body: 'Allottees who have completed payment of 100% premium can now apply for possession online through the portal.', attachments: ['Possession-SOP.pdf'] },
+ { id: 5, title: 'Authority Circular: e-Challan mandatory for all dues', cat: 'Authority Circular', date: '02 May 2026', pinned: false, body: 'With effect from 1 June 2026, all payments must be made via e-Challan generated through the portal. Offline challans will be discontinued.', attachments: [] },
+ ];
+
+ const activity = [
+ { icon: 'payments', color: 'emerald', title: 'Payment received', desc: 'Location Premium Instalment 13/16 — ₹96,560', time: '12 Mar 2026' },
+ { icon: 'upload', color: 'blue', title: 'Documents uploaded', desc: 'Building plan & structural certificate', time: '15 Feb 2026' },
+ { icon: 'construction', color: 'amber', title: 'Construction application submitted', desc: 'Under authority review', time: '16 Feb 2026' },
+ { icon: 'check', color: 'emerald', title: 'Fire NOC approved', desc: 'Valid till Mar 2031', time: '02 Mar 2026' },
+ { icon: 'notices', color: 'navy', title: 'New notice published', desc: 'Revised lease rent rates FY 2026-27', time: '01 Jun 2026' },
+ ];
+
+ const sectors = [
+ { id: '18', name: 'Sector 18', type: 'Residential', plots: 1240, status: 'Developed', mine: true },
+ { id: '20', name: 'Sector 20', type: 'Residential', plots: 1810, status: 'Developed', mine: false },
+ { id: '22D', name: 'Sector 22D', type: 'Institutional', plots: 420, status: 'Developing', mine: false },
+ { id: '24A', name: 'Sector 24A', type: 'Industrial', plots: 690, status: 'Developing', mine: false },
+ { id: '25', name: 'Sector 25', type: 'Mixed Use', plots: 980, status: 'Developed', mine: false },
+ { id: '28', name: 'Sector 28', type: 'Residential', plots: 1530, status: 'Planned', mine: false },
+ { id: '29', name: 'Sector 29', type: 'Commercial', plots: 350, status: 'Planned', mine: false },
+ ];
+
+ const transferFees = [
+ { label: 'Transfer Charges (2.5% of value)', amount: 193125 },
+ { label: 'Processing Fee', amount: 5000 },
+ { label: 'Documentation Charges', amount: 2500 },
+ { label: 'GST (18% on fees)', amount: 1350 },
+ ];
+
+ const assistantSuggestions = [
+ 'How do I start construction?',
+ 'What documents are required?',
+ 'Show latest notices',
+ 'How can I transfer my plot?',
+ 'What payments are pending?',
+ 'Show Sector 18 map',
+ ];
+
+ const tickets = [
+ { id: 'TKT-4821', subject: 'Lease deed correction request', status: 'Open', priority: 'High', updated: '2h ago' },
+ { id: 'TKT-4790', subject: 'Challan not reflecting after UPI payment', status: 'Resolved', priority: 'Medium', updated: '3d ago' },
+ { id: 'TKT-4755', subject: 'Building plan re-submission query', status: 'In Progress', priority: 'Medium', updated: '5d ago' },
+ ];
+
+ // Support inbox — secure message threads with authority officers
+ const supportThreads = [
+ {
+ id: 'MSG-4821', subject: 'Lease deed correction request',
+ agent: { name: 'Anjali Sharma', role: 'Records Officer', initials: 'AS', online: true },
+ unread: 2, time: '2h ago',
+ messages: [
+ { from: 'me', text: 'There is a spelling error in the name on my registered lease deed. How can I get it corrected?', time: 'Mon 10:12' },
+ { from: 'agent', text: 'Hello Rajeev, thanks for reaching out. We can process a name-correction request against allotment YEA-654321. Could you share a self-attested ID proof?', time: 'Mon 10:21' },
+ { from: 'me', text: 'Sure, I have uploaded my Aadhaar in the Documents section.', time: 'Mon 10:25' },
+ { from: 'agent', text: 'Received. Your correction request is logged as TKT-4821 and forwarded to the Records cell. Expected TAT is 7 working days.', time: 'Mon 10:31' },
+ ],
+ },
+ {
+ id: 'MSG-4790', subject: 'Challan not reflecting after UPI payment',
+ agent: { name: 'Vikram Singh', role: 'Payments Desk', initials: 'VS', online: true },
+ unread: 0, time: 'Yesterday',
+ messages: [
+ { from: 'me', text: 'I paid ₹38,625 via UPI yesterday but the challan is not showing under Payments.', time: 'Tue 16:40' },
+ { from: 'agent', text: 'Apologies for the delay — UPI reconciliations can take up to 24 hours. I can see the payment now; challan CH-2026-0128-04 has been generated against your account.', time: 'Tue 17:02' },
+ { from: 'me', text: 'Great, I can see it now. Thank you!', time: 'Tue 17:05' },
+ ],
+ },
+ {
+ id: 'MSG-4755', subject: 'Building plan re-submission query',
+ agent: { name: 'Neha Gupta', role: 'Construction Cell', initials: 'NG', online: false },
+ unread: 1, time: '5d ago',
+ messages: [
+ { from: 'me', text: 'My building plan was returned for revision. What changes are required?', time: 'Thu 11:15' },
+ { from: 'agent', text: 'The rear-side setback needs to be increased to 3m as per the updated FAR norms. Please revise and re-upload the architect-stamped plan.', time: 'Thu 12:02' },
+ ],
+ },
+ ];
+
+ // Support team — shown in the "Your support team" section (AI + human officers)
+ const supportTeam = [
+ { name: 'Aarav', role: 'Chief AI Support', initials: 'AI', online: true, ai: true, desc: 'Instant answers, available 24/7' },
+ { name: 'Anjali Sharma', role: 'Records Officer', initials: 'AS', online: true, desc: 'Lease deed, allotment & records' },
+ { name: 'Vikram Singh', role: 'Payments Desk', initials: 'VS', online: true, desc: 'Challans, dues & refunds' },
+ { name: 'Neha Gupta', role: 'Construction Cell', initials: 'NG', online: false, desc: 'Building plans & approvals' },
+ ];
+
+ // Official authority deposit/challan records (read-only, from authority backend) —
+ // shown immediately after login to build trust
+ const authorityRecords = [
+ { id: 'AR-2019-001', date: '14 Mar 2019', head: 'Registration / Earnest Money', challan: 'CH-2019-0314-01', amount: 772500, mode: 'Demand Draft', status: 'Deposited' },
+ { id: 'AR-2019-014', date: '20 Aug 2019', head: 'Allotment Premium (Lump-sum 25%)', challan: 'CH-2019-0820-14', amount: 1158750, mode: 'RTGS', status: 'Deposited' },
+ { id: 'AR-2021-077', date: '12 Jul 2021', head: 'Location Premium Instalment 6/16', challan: 'CH-2021-0712-77', amount: 96560, mode: 'Net Banking', status: 'Deposited' },
+ { id: 'AR-2024-052', date: '30 Jun 2024', head: 'Annual Lease Rent 2023-24', challan: 'CH-2024-0630-52', amount: 33500, mode: 'UPI', status: 'Deposited' },
+ { id: 'AR-2025-091', date: '12 Sep 2025', head: 'Location Premium Instalment 12/16', challan: 'CH-2025-0912-21', amount: 96560, mode: 'Net Banking', status: 'Deposited' },
+ ];
+
+ // Construction journey state — drives the first-time "what next?" experience
+ const journey = {
+ constructionStarted: true, // toggleable in the demo via the journey screen
+ stage: 'Authority Review',
+ nextAction: 'Upload pending Soil Test Report',
+ progressPct: 41,
+ };
+
+ // Architectural sample plans (pre-construction guidance)
+ const samplePlans = [
+ { id: 'SP-300-E', name: 'Aangan — Courtyard Villa', size: '300 sq.m', facing: 'East', floors: 'G+1', beds: 4, style: 'Contemporary', tour: true, accent: '#1d5fbf' },
+ { id: 'SP-300-N', name: 'Surya — Sunlit Modern', size: '300 sq.m', facing: 'North', floors: 'G+2', beds: 5, style: 'Modern', tour: true, accent: '#0f9d6b' },
+ { id: 'SP-200-E', name: 'Nivaas — Compact Smart', size: '200 sq.m', facing: 'East', floors: 'G+1', beds: 3, style: 'Minimal', tour: false, accent: '#c77a08' },
+ { id: 'SP-450-S', name: 'Vista — Luxury Duplex', size: '450 sq.m', facing: 'South', floors: 'G+2', beds: 6, style: 'Luxury', tour: true, accent: '#103263' },
+ { id: 'SP-200-W', name: 'Saral — Efficient Living', size: '200 sq.m', facing: 'West', floors: 'G+1', beds: 3, style: 'Contemporary', tour: false, accent: '#7a3fbf' },
+ { id: 'SP-300-S', name: 'Prakriti — Green Home', size: '300 sq.m', facing: 'South', floors: 'G+1', beds: 4, style: 'Eco / Sustainable', tour: true, accent: '#0a7d56' },
+ ];
+
+ // Construction financials & site (Section B dashboard widgets)
+ const construction = {
+ estimatedCost: 9800000,
+ spent: 4018000,
+ contractor: { name: 'Shreeji Constructions Pvt. Ltd.', status: 'On Site', rating: 4.6, since: 'Feb 2026' },
+ completionForecast: 'Mar 2027',
+ onSchedule: true,
+ currentMilestone: 'Ground Floor Structure',
+ siteUpdates: [
+ { title: 'RCC slab casting completed', date: '04 Jun 2026', icon: 'check', color: 'emerald' },
+ { title: 'Steel reinforcement inspection passed', date: '28 May 2026', icon: 'shield', color: 'blue' },
+ { title: 'Brickwork — ground floor started', date: '20 May 2026', icon: 'construction', color: 'amber' },
+ ],
+ };
+
+ // Construction-side payments (milestones / invoices / receipts)
+ const constructionPayments = [
+ { id: 'CINV-014', milestone: 'Ground Floor Structure (40%)', vendor: 'Shreeji Constructions', date: '02 Jun 2026', amount: 1568000, status: 'Paid' },
+ { id: 'CINV-009', milestone: 'Foundation & Plinth (25%)', vendor: 'Shreeji Constructions', date: '14 Apr 2026', amount: 2450000, status: 'Paid' },
+ { id: 'CINV-018', milestone: 'First Floor Structure (20%)', vendor: 'Shreeji Constructions', date: 'Due 15 Aug 2026', amount: 1960000, status: 'Upcoming' },
+ { id: 'CINV-006', milestone: 'Architect & Design Fees', vendor: 'Studio Aangan', date: '10 Feb 2026', amount: 320000, status: 'Paid' },
+ ];
+
+ // Onboarding decision — 7 action cards
+ const onboardingActions = [
+ { id: 'plot', icon: 'plot', title: 'View My Plot Status', desc: 'Authority records, lease deed & status', route: 'plot', color: 'blue' },
+ { id: 'paperwork', icon: 'documents', title: 'Complete Authority Paperwork', desc: 'ATL, lease deed & pending formalities', route: 'documents', color: 'navy' },
+ { id: 'construction', icon: 'construction', title: 'Start Construction', desc: 'Plans, approvals & build tracking', route: 'construction', color: 'amber' },
+ { id: 'plans', icon: 'grid', title: 'View Sample Plans', desc: 'Architect designs for your plot', route: 'sampleplans', color: 'emerald' },
+ { id: 'tours', icon: 'eye', title: 'Explore Virtual Tours', desc: '3D walkthroughs of ready designs', route: 'sampleplans', color: 'blue' },
+ { id: 'sell', icon: 'transfer', title: 'Sell My Plot', desc: 'Eligibility, fees & transfer application', route: 'transfer', color: 'navy' },
+ { id: 'expert', icon: 'support', title: 'Contact Expert', desc: 'Talk to a YEIDA support officer', route: 'support', color: 'emerald' },
+ ];
+
+ const relationshipOptions = ['Self', 'Son', 'Daughter', 'Spouse', 'Relative', 'Authorized Representative'];
+
+ // ---- Construction Journey (8-step survey workflow) ----
+ const journeyGoals = [
+ { id: 'start', icon: 'construction', title: 'Start construction soon', desc: 'Begin your build journey with plans & approvals' },
+ { id: 'completion', icon: 'checkCircle', title: 'Obtain a completion certificate', desc: 'Finalise an existing or finished build' },
+ { id: 'full', icon: 'building', title: 'Complete full construction', desc: 'End-to-end managed construction' },
+ { id: 'consult', icon: 'support', title: 'Need construction consultation', desc: 'Talk to an expert before deciding' },
+ { id: 'explore', icon: 'grid', title: 'Explore design options', desc: 'Browse plans & 3D walkthroughs first' },
+ ];
+
+ const journeySteps = [
+ { key: 'goal', title: 'Goal Selection', desc: 'What you want to achieve', icon: 'flag',
+ summary: 'Start construction soon', done: true },
+ { key: 'plot', title: 'Plot Verification', desc: 'Confirm your allotment & plot', icon: 'building',
+ summary: 'YEA-654321 · Plot B-47, Sector 18', done: true },
+ { key: 'design', title: 'Design Selection', desc: 'Choose a house design', icon: 'grid',
+ summary: 'Aangan — Courtyard Villa (G+1)', done: true },
+ { key: 'planning', title: 'Construction Planning', desc: 'Scope, timeline & permissions', icon: 'calendar',
+ summary: 'G+1 · est. 11 months · plan sanctioned', done: true },
+ { key: 'budget', title: 'Budget & Payment Setup', desc: 'Cost plan & milestones', icon: 'rupee',
+ summary: '₹98,00,000 budget · 5 milestones', done: false, active: true },
+ { key: 'contractor', title: 'Contractor Assignment', desc: 'Assign your project team', icon: 'users',
+ summary: 'Pending assignment', done: false },
+ { key: 'progress', title: 'Construction Progress', desc: 'Track on-site milestones', icon: 'trend',
+ summary: 'Not started', done: false },
+ { key: 'certificate', title: 'Completion Certificate', desc: 'Final inspection & handover', icon: 'checkCircle',
+ summary: 'Not started', done: false },
+ ];
+
+ const projectTeam = [
+ { role: 'Project Manager', name: 'Anil Verma', org: 'YEIDA PMC', phone: '+91 98xxxxxx01', avatar: 'AV', online: true },
+ { role: 'Site Engineer', name: 'Priya Nair', org: 'Shreeji Constructions', phone: '+91 98xxxxxx02', avatar: 'PN', online: true },
+ { role: 'Architect', name: 'Studio Aangan', org: 'Lead: R. Mehta', phone: '+91 98xxxxxx03', avatar: 'SA', online: false },
+ { role: 'Construction Team', name: '12-member crew', org: 'Shreeji Constructions', phone: '+91 98xxxxxx04', avatar: 'CT', online: true },
+ ];
+
+ const teamUpdates = [
+ { who: 'Priya Nair', text: 'RCC slab casting for ground floor completed and cured.', time: '2h ago', avatar: 'PN' },
+ { who: 'Anil Verma', text: 'First-floor material procurement scheduled for next week.', time: 'Yesterday', avatar: 'AV' },
+ { who: 'Studio Aangan', text: 'Revised electrical layout shared for approval.', time: '3 days ago', avatar: 'SA' },
+ ];
+
+ // Project Progress Center — visual construction stages
+ const progressStages = [
+ { name: 'Site Cleaning', pct: 100, date: '02 Mar 2026', note: 'Site cleared & leveled', photos: 4 },
+ { name: 'Excavation', pct: 100, date: '14 Mar 2026', note: 'Foundation trench dug', photos: 6 },
+ { name: 'Foundation', pct: 100, date: '04 Apr 2026', note: 'PCC & footing laid', photos: 8 },
+ { name: 'Boundary Wall', pct: 100, date: '20 Apr 2026', note: 'Perimeter wall complete', photos: 3 },
+ { name: 'Columns', pct: 100, date: '06 May 2026', note: 'GF columns cast', photos: 5 },
+ { name: 'Slab Casting', pct: 65, date: 'In progress', note: 'GF roof slab — curing', photos: 7 },
+ { name: 'Brick Work', pct: 20, date: 'Started', note: 'GF walls begun', photos: 2 },
+ { name: 'Electrical', pct: 0, date: 'Upcoming', note: '', photos: 0 },
+ { name: 'Plumbing', pct: 0, date: 'Upcoming', note: '', photos: 0 },
+ { name: 'Finishing', pct: 0, date: 'Upcoming', note: '', photos: 0 },
+ { name: 'Completion', pct: 0, date: 'Est. Mar 2027', note: '', photos: 0 },
+ ];
+ // ---- KYC documents (collected when the allottee becomes a customer, after contract signing) ----
+ // proof: 'identity' | 'address' · expires: false for Aadhaar/PAN, true for Passport/DL
+ const kycDocTypes = [
+ { id: 'aadhaar', label: 'Aadhaar', proof: 'identity', alsoAddress: true, expires: false, hint: 'Identity + address proof · does not expire' },
+ { id: 'pan', label: 'PAN', proof: 'identity', alsoAddress: false, expires: false, hint: 'Identity proof · does not expire' },
+ { id: 'passport', label: 'Passport', proof: 'identity', alsoAddress: true, expires: true, hint: 'Identity + address proof · has an expiry date' },
+ { id: 'dl', label: 'Driving Licence', proof: 'identity', alsoAddress: true, expires: true, hint: 'Identity + address proof · has an expiry date' },
+ { id: 'utility', label: 'Electricity Bill', proof: 'address', alsoAddress: true, expires: true, hint: 'Address proof · recent bill (within 3 months)' },
+ { id: 'water', label: 'Water Bill', proof: 'address', alsoAddress: true, expires: true, hint: 'Address proof · recent bill (within 3 months)' },
+ { id: 'housetax', label: 'House Tax Receipt', proof: 'address', alsoAddress: true, expires: false, hint: 'Address proof · latest receipt' },
+ { id: 'bankstmt', label: 'Bank Statement', proof: 'address', alsoAddress: true, expires: true, hint: 'Address proof · recent statement showing your name & address' },
+ { id: 'photo', label: 'Photograph', proof: 'photo', alsoAddress: false, expires: false, hint: 'Recent passport-size photograph' },
+ ];
+
+ // current allottee KYC state (mock) — drives the KYC tab
+ const kyc = {
+ status: 'verified', // verified | pending | action_required
+ verifiedOn: '12 Mar 2019',
+ contractSigned: true, // KYC is collected only after the contract is signed
+ viaPOA: false, // contract signed by one person, KYC completed for another via Power of Attorney
+ documents: [
+ { type: 'aadhaar', number: 'xxxx xxxx 4821', proof: 'identity+address', status: 'verified', expiry: null },
+ { type: 'pan', number: 'ABCDE****F', proof: 'identity', status: 'verified', expiry: null },
+ { type: 'passport', number: 'M12****9', proof: 'identity+address', status: 'verified', expiry: '14 Aug 2028' },
+ ],
+ };
+
+ // Bank account on file — required for invoices / refunds / construction payouts
+ const bankAccount = {
+ holder: 'Rajeev Malhotra', bank: 'HDFC Bank', branch: 'Sector 18, Noida',
+ accountMasked: 'xxxxxx7841', ifsc: 'HDFC0001234', type: 'Savings', verified: true,
+ };
+
+ // Contact preferences — primary + secondary email & mobile.
+ // The allottee may be a different person from the account holder (e.g. an allottee
+ // living abroad while the spouse manages the account in India). Each contact is
+ // tagged with an `owner` ('holder' | 'allottee') so reminders can be routed to the
+ // allottee's own number/email if desired. Each payment-reminder channel maps to a
+ // specific contact (its own column/slot), and at least one channel must stay enabled.
+ const contactPrefs = {
+ emails: [
+ { id: 'e1', value: 'r.malhotra@email.com', primary: true, verified: true, owner: 'holder' },
+ { id: 'e2', value: 'rajeev.work@company.com', primary: false, verified: true, owner: 'holder' },
+ { id: 'e3', value: 'allottee.nri@email.com', primary: false, verified: true, owner: 'allottee' },
+ ],
+ phones: [
+ { id: 'p1', value: '+91 98xxxxxx12', primary: true, verified: true, owner: 'holder' },
+ { id: 'p2', value: '+91 99xxxxxx77', primary: false, verified: false, owner: 'holder' },
+ { id: 'p3', value: '+971 50xxxxxx89', primary: false, verified: true, owner: 'allottee' },
+ ],
+ // channel → { on, to: contactId }. 'to' selects which email/phone slot receives it
+ // (can be a holder's or the allottee's contact). 'post' goes to the mailing address.
+ // Five notification channels; at least one must always stay enabled.
+ reminders: {
+ email: { on: true, to: 'e1' },
+ sms: { on: true, to: 'p1' },
+ whatsapp: { on: false, to: 'p1' },
+ call: { on: false, to: 'p1' },
+ post: { on: false },
+ },
+ marketing: { newsletter: false, promotions: false, productUpdates: true },
+ };
+
+ // Recent login / security activity log (device · IP · location)
+ const loginActivity = [
+ { action: 'Successful login', device: 'MacBook Pro · Chrome', ip: '49.36.x.x', location: 'Greater Noida, IN', time: 'Today, 09:14', ok: true },
+ { action: 'Password changed', device: 'MacBook Pro · Chrome', ip: '49.36.x.x', location: 'Greater Noida, IN', time: 'Yesterday, 18:02', ok: true },
+ { action: 'New device login', device: 'iPhone 14 · YEIDA App', ip: '49.36.x.x', location: 'Greater Noida, IN', time: '2 days ago', ok: true },
+ { action: 'Failed login attempt', device: 'Unknown · Firefox', ip: '103.21.x.x', location: 'Mumbai, IN', time: '4 days ago', ok: false },
+ { action: 'Login alert consent enabled', device: 'Windows PC · Edge', ip: '157.32.x.x', location: 'Delhi, IN', time: '5 days ago', ok: true },
+ ];
+
+ // Elevation variants — front-facade options, vary by plot size & orientation
+ const elevations = [
+ { id: 'EL-A', name: 'Modern Cube', suitsSize: '300 sq.m', suitsFacing: 'East', floors: 'G+1', accent: '#1d5fbf', desc: 'Clean lines, large glazing, flat parapet' },
+ { id: 'EL-B', name: 'Contemporary Stone', suitsSize: '300 sq.m', suitsFacing: 'East', floors: 'G+1', accent: '#0f9d6b', desc: 'Stone-clad base with cantilever balcony' },
+ { id: 'EL-C', name: 'Courtyard Classic', suitsSize: '300 sq.m', suitsFacing: 'North', floors: 'G+2', accent: '#c77a08', desc: 'Pitched accents, deep verandah' },
+ { id: 'EL-D', name: 'Compact Smart', suitsSize: '200 sq.m', suitsFacing: 'East', floors: 'G+1', accent: '#7a3fbf', desc: 'Efficient footprint, vertical louvers' },
+ { id: 'EL-E', name: 'Luxury Duplex', suitsSize: '450 sq.m', suitsFacing: 'South', floors: 'G+2', accent: '#103263', desc: 'Double-height portico, twin balconies' },
+ ];
+
+ // Daily site photo gallery — folder (Drive-style) layout
+ const galleryFolders = [
+ { id: 'f-jun', name: 'June 2026', date: '01–14 Jun', count: 18, cover: '#1d5fbf' },
+ { id: 'f-may', name: 'May 2026', date: '01–31 May', count: 42, cover: '#0f9d6b' },
+ { id: 'f-apr', name: 'April 2026', date: '01–30 Apr', count: 36, cover: '#c77a08' },
+ { id: 'f-mar', name: 'March 2026', date: '02–31 Mar', count: 24, cover: '#103263' },
+ { id: 'f-docs', name: 'Inspection Reports', date: 'Mixed', count: 9, cover: '#7a3fbf' },
+ ];
+
+ const countryCodes = [
+ { code: '+91', label: 'IN', name: 'India', digits: 10, example: '98765 43210' },
+ { code: '+971', label: 'AE', name: 'UAE', digits: 9, example: '50 123 4567' },
+ { code: '+1', label: 'US', name: 'United States', digits: 10, example: '(201) 555-0123' },
+ { code: '+44', label: 'UK', name: 'United Kingdom', digits: 10, example: '7400 123456' },
+ { code: '+65', label: 'SG', name: 'Singapore', digits: 8, example: '8123 4567' },
+ ];
+
+ // ---- Address / location data (drives country → state → dependent fields) ----
+ const indiaStates = [
+ 'Andhra Pradesh', 'Assam', 'Bihar', 'Chhattisgarh', 'Delhi', 'Goa', 'Gujarat', 'Haryana',
+ 'Himachal Pradesh', 'Jharkhand', 'Karnataka', 'Kerala', 'Madhya Pradesh', 'Maharashtra',
+ 'Odisha', 'Punjab', 'Rajasthan', 'Tamil Nadu', 'Telangana', 'Uttar Pradesh', 'Uttarakhand', 'West Bengal',
+ ];
+ const usStates = ['California', 'Florida', 'Illinois', 'New Jersey', 'New York', 'Texas', 'Washington'];
+ const ukRegions = ['England', 'Scotland', 'Wales', 'Northern Ireland'];
+ const emirates = ['Abu Dhabi', 'Dubai', 'Sharjah', 'Ajman', 'Ras Al Khaimah', 'Fujairah', 'Umm Al Quwain'];
+
+ // country → how the postal address is captured.
+ // 'pincode' = India: 6-digit PIN auto-fills city & state
+ // 'postcode' = US/UK: postcode lookup service returns address candidates
+ // 'manual' = free entry (no lookup service available)
+ const addressCountries = [
+ { code: 'IN', name: 'India', lookup: 'pincode', postalLabel: 'PIN Code', postalLen: 6, states: indiaStates },
+ { code: 'US', name: 'United States', lookup: 'postcode', postalLabel: 'ZIP Code', postalLen: 5, states: usStates },
+ { code: 'GB', name: 'United Kingdom', lookup: 'postcode', postalLabel: 'Postcode', postalLen: 8, states: ukRegions },
+ { code: 'AE', name: 'United Arab Emirates', lookup: 'manual', postalLabel: 'PO Box', postalLen: 6, states: emirates },
+ { code: 'SG', name: 'Singapore', lookup: 'manual', postalLabel: 'Postal Code', postalLen: 6, states: [] },
+ ];
+
+ // mock PIN → locality (India auto-fill on 6-digit entry)
+ const pincodeLookup = {
+ '203201': { city: 'Greater Noida', district: 'Gautam Buddh Nagar', state: 'Uttar Pradesh' },
+ '201301': { city: 'Noida', district: 'Gautam Buddh Nagar', state: 'Uttar Pradesh' },
+ '110001': { city: 'New Delhi', district: 'Central Delhi', state: 'Delhi' },
+ '122001': { city: 'Gurugram', district: 'Gurugram', state: 'Haryana' },
+ '226001': { city: 'Lucknow', district: 'Lucknow', state: 'Uttar Pradesh' },
+ '302001': { city: 'Jaipur', district: 'Jaipur', state: 'Rajasthan' },
+ };
+
+ // mock postcode → address candidates (US/UK "find address" service)
+ const postcodeLookup = {
+ '10001': [
+ { line1: '350 5th Avenue', city: 'New York', state: 'New York' },
+ { line1: '20 W 34th Street', city: 'New York', state: 'New York' },
+ ],
+ '94105': [
+ { line1: '101 California Street', city: 'San Francisco', state: 'California' },
+ { line1: '1 Market Street', city: 'San Francisco', state: 'California' },
+ ],
+ 'SW1A1AA': [
+ { line1: 'Buckingham Palace', city: 'London', state: 'England' },
+ ],
+ 'EC1A1BB': [
+ { line1: "St Paul's Churchyard", city: 'London', state: 'England' },
+ ],
+ };
+
+ // ---- Legal documents (open in a popup; acceptance enabled only after review) ----
+ const legalDocs = {
+ terms: {
+ title: 'Terms & Conditions', updated: '01 Apr 2026',
+ sections: [
+ { h: '1. Acceptance of Terms', p: 'By registering for and using the YEIDA Allottee Portal ("the Portal") you agree to be bound by these Terms & Conditions and all applicable rules and regulations of the Yamuna Expressway Industrial Development Authority. If you do not agree, you must not use the Portal.' },
+ { h: '2. Eligibility & Account', p: 'Access is restricted to verified allottees and their duly authorised representatives. You are responsible for maintaining the confidentiality of your credentials and for all activity that occurs under your account. Allotment, sector, pocket and plot identifiers are linked to authority records and cannot be self-edited after verification.' },
+ { h: '3. Use of the Portal', p: 'The Portal is provided for managing your allotment, payments, documents and construction approvals. You agree not to misuse the Portal, attempt unauthorised access, or upload unlawful or misleading content. The Authority may suspend access for any breach.' },
+ { h: '4. Payments & Charges', p: 'All dues, premiums, lease rent and fees displayed are indicative and subject to reconciliation with authority records. Online payments are processed through authorised payment gateways; the Authority is not liable for failures attributable to your bank or the gateway.' },
+ { h: '5. Limitation of Liability', p: 'The Portal and its content are provided "as is". To the extent permitted by law, the Authority disclaims liability for any indirect or consequential loss arising from use of the Portal.' },
+ { h: '6. Governing Law', p: 'These Terms are governed by the laws of India and subject to the exclusive jurisdiction of the courts at Gautam Buddh Nagar, Uttar Pradesh.' },
+ ],
+ },
+ privacy: {
+ title: 'Privacy Policy', updated: '01 Apr 2026',
+ sections: [
+ { h: '1. Information We Collect', p: 'We collect the personal details you provide during registration (name, email, mobile, addresses, relationship to allottee), identity and KYC documents, and records linked to your allotment such as payments and construction filings.' },
+ { h: '2. How We Use Your Information', p: 'Your information is used to verify your identity, link your allotment, process payments and applications, send statutory notices and reminders, and provide support. We send payment reminders and alerts to your selected primary email and phone.' },
+ { h: '3. Cookies', p: 'We use strictly necessary cookies to keep you signed in and secure your session. With your consent we also use preference and analytics cookies to improve the Portal. You can manage non-essential cookies at any time from the cookie banner.' },
+ { h: '4. Data Sharing', p: 'We do not sell your data. Information may be shared with authorised authority officers, payment gateways and government bodies strictly as required to deliver portal services or comply with law.' },
+ { h: '5. Your Rights', p: 'You may review and update your contact details, manage communication preferences, and request a copy of your data. Allotment identity fields remain locked to authority records after verification.' },
+ { h: '6. Data Security & Retention', p: 'We apply reasonable technical and organisational safeguards, including OTP-based verification. Records are retained as long as required for allotment administration and statutory compliance.' },
+ ],
+ },
+ };
+
+ return {
+ fmtINR, user, authorityStatus, STATUS_CODES, authorityRecords, journey, samplePlans,
+ relationshipOptions, countryCodes, construction, constructionPayments, onboardingActions,
+ addressCountries, pincodeLookup, postcodeLookup, legalDocs, indiaStates,
+ journeyGoals, journeySteps, projectTeam, teamUpdates, progressStages,
+ property, dues, payments, constructionSteps, requiredDocs,
+ constructionMilestones, documents, notices, activity, sectors, transferFees,
+ assistantSuggestions, tickets, supportThreads, supportTeam,
+ kycDocTypes, kyc, bankAccount, contactPrefs, loginActivity, elevations, galleryFolders,
+ };
+})();
+
+export default YEIDA;
+export { YEIDA };
diff --git a/src/components/portal/documents.jsx b/src/components/portal/documents.jsx
new file mode 100644
index 0000000..cb5874f
--- /dev/null
+++ b/src/components/portal/documents.jsx
@@ -0,0 +1,178 @@
+'use client';
+/* Document Center */
+import React, { useState, useRef } from 'react';
+import Icon from './icons';
+import { PageHead, Logo } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA;
+
+ const CATS = ['All', 'Allotment', 'Possession', 'Lease Deed', 'Building Approvals', 'NOCs', 'Circulars', 'Master Plans', 'Government Orders'];
+
+ function Documents({ toast }) {
+ const [cat, setCat] = useState('All');
+ const [q, setQ] = useState('');
+ const [view, setView] = useState('grid');
+ const [bookmarks, setBookmarks] = useState({});
+ const [preview, setPreview] = useState(null);
+ const tagColor = { blue: ['var(--blue-50)', 'var(--blue-600)'], green: ['var(--emerald-50)', 'var(--emerald-700)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'] };
+
+ const list = D.documents.filter(d => (cat === 'All' || d.type === cat) && d.name.toLowerCase().includes(q.toLowerCase()));
+
+ return (
+
+
+
+ {[['grid', 'grid'], ['list', 'menu']].map(([k, ic]) => (
+ setView(k)} className="icon-btn" style={{ width: 34, height: 34, background: view === k ? '#fff' : 'transparent', boxShadow: view === k ? 'var(--sh-xs)' : 'none', color: view === k ? 'var(--ink)' : 'var(--muted)' }}>
+ ))}
+
+
+
+ {/* search + filters */}
+
+
+
+
+ setQ(e.target.value)} />
+
+
Filters
+
+
+ {CATS.map(c => setCat(c)}>{c} )}
+
+
+
+
+
+ {view === 'grid' ? (
+
+ {list.map((d, i) => {
+ const tc = tagColor[d.tag];
+ return (
+
+
setBookmarks(b => ({ ...b, [d.name]: !b[d.name] }))} style={{ position: 'absolute', top: 14, right: 14, background: 'none', border: 'none', cursor: 'pointer', color: bookmarks[d.name] ? 'var(--amber-500)' : 'var(--faint)' }}>
+
+
+
+
{d.name}
+
{d.type}
+
{d.date} {d.size}
+
+ setPreview(d)}> Preview
+ toast && toast('Downloading ' + d.name)}>
+
+
+ );
+ })}
+
+ ) : (
+
+
+ Document Category Date Size
+
+ {list.map((d, i) => {
+ const tc = tagColor[d.tag];
+ return (
+
+ {d.name}
+ {d.type} {d.date} {d.size}
+ setPreview(d)}> toast && toast('Downloading…')}>
+
+ );
+ })}
+
+
+
+ )}
+
+ {preview &&
setPreview(null)} toast={toast} />}
+
+ );
+ }
+
+ function PreviewModal({ doc, onClose, toast }) {
+ return (
+
+
e.stopPropagation()} className="anim-pop card" style={{ width: 720, maxWidth: '100%', maxHeight: '88vh', padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
+
+
{doc.name}
{doc.type} · {doc.size}
+
toast && toast('Downloaded')}> Download
+
+
+ {/* faux document page */}
+
+
+
Yamuna Expressway Industrial Development Authority
+
+
{doc.name}
+ {[...Array(8)].map((_, i) =>
)}
+
+ {[...Array(4)].map((_, i) =>
)}
+
+
+
+
+
+ );
+ }
+
+ /* Required documents — uploaded items show GREEN, pending items show RED until a file is attached */
+ function RequiredUploads({ toast }) {
+ const [docs, setDocs] = useState(() => D.requiredDocs.map(d => ({ ...d })));
+ const [q, setQ] = useState('');
+ const refs = useRef({});
+ const done = docs.filter(d => d.status === 'uploaded').length;
+
+ const pick = (name) => refs.current[name] && refs.current[name].click();
+ const onFile = (name, e) => {
+ const file = e.target.files && e.target.files[0];
+ if (!file) return;
+ setDocs(arr => arr.map(d => d.name === name ? { ...d, status: 'uploaded', file: file.name } : d));
+ toast && toast('Uploaded · ' + name);
+ };
+
+ const list = docs.filter(d => d.name.toLowerCase().includes(q.toLowerCase()));
+
+ return (
+
+
+
+
Required Documents
+
Items stay red until uploaded, then turn green .
+
+
+
setQ(e.target.value)} />
+
{done}/{docs.length} uploaded
+
+
+
+
+ {list.map(d => {
+ const ok = d.status === 'uploaded';
+ const c = ok ? ['var(--emerald-50)', 'var(--emerald-600)', '#bfe6d3'] : ['var(--red-50)', 'var(--red-600)', '#f3c2c2'];
+ return (
+
+
+
+
+
{d.name}
+
{ok && d.file ? d.file : d.note}
+
+
+
+ (refs.current[d.name] = el)} type="file" accept=".pdf,.jpg,.jpeg,.png" onChange={e => onFile(d.name, e)} style={{ display: 'none' }} />
+ pick(d.name)}>
+ {ok ? 'Replace' : 'Upload'}
+
+
+
+ );
+ })}
+
+
+ );
+ }
+
+export { Documents };
+export default Documents;
diff --git a/src/components/portal/extras.jsx b/src/components/portal/extras.jsx
new file mode 100644
index 0000000..60dba71
--- /dev/null
+++ b/src/components/portal/extras.jsx
@@ -0,0 +1,1114 @@
+'use client';
+/* Notices, Sector Maps, Support, Profile */
+import React, { useState, useRef, useEffect } from 'react';
+import Icon from './icons';
+import { PageHead, Avatar, useToast } from './ui';
+import { SectorMap } from './map';
+import { useUserPhoto, readImageFile, useUser, setUserProfile } from './user-store';
+import YEIDA from './data';
+
+const D = YEIDA;
+const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ /* ---------------- NOTICES ---------------- */
+ const NCATS = ['All', 'Policy Update', 'Construction Guidelines', 'Sector Update', 'Authority Circular', 'Announcement'];
+ function Notices() {
+ const [cat, setCat] = useState('All');
+ const [sel, setSel] = useState(D.notices[0]);
+ const list = D.notices.filter(n => cat === 'All' || n.cat === cat);
+ return (
+
+
+
+ {NCATS.map(c => setCat(c)}>{c} )}
+
+
+
+ {list.map(n => (
+
setSel(n)} className="card card-hover" style={{ textAlign: 'left', padding: '16px 18px', cursor: 'pointer', border: sel.id === n.id ? '1.5px solid var(--blue-400)' : '1px solid var(--line)', background: sel.id === n.id ? 'var(--blue-50)' : '#fff' }}>
+
+ {n.cat}
+ {n.pinned && }
+
+ {n.title}
+ {n.date}{n.attachments.length > 0 && ' · ' + n.attachments.length + ' attachment' + (n.attachments.length > 1 ? 's' : '')}
+
+ ))}
+
+
+
+
{sel.title}
+
{sel.date} YEIDA Authority
+
+
{sel.body}
+ {sel.attachments.length > 0 && (
+
+
Attachments
+
+ {sel.attachments.map(a => (
+
+ {a}
+ Download
+
+ ))}
+
+
+ )}
+
+
+
+ );
+ }
+
+ /* ---------------- SECTOR MAPS ---------------- */
+ function SectorMaps({ go }) {
+ const [active, setActive] = useState(D.sectors[0]);
+ return (
+
+
+
+ {D.sectors.map(s => (
+ setActive(s)} className={'chip' + (active.id === s.id ? ' active' : '')} style={{ flex: 'none' }}>
+ {s.mine && }{s.name}
+
+ ))}
+
+
+
+
+
+
{active.name} {active.mine && Your sector }
+
+ {[['Type', active.type], ['Total Plots', active.plots.toLocaleString('en-IN')], ['Status', active.status], ['Location', 'Yamuna Expressway']].map(([l, v]) => (
+
{l} {v}
+ ))}
+
+
+
+
Map Layers
+
+ {[['Road Network', 'map'], ['Infrastructure', 'building'], ['Master Plan Overlay', 'grid'], ['Satellite View', 'layers']].map(([l, ic]) => (
+
{l}
+ ))}
+
+
+
+
+
Master Plan PDF
+
Download the official {active.name} master plan.
+
go('documents')}> Download
+
+
+
+
+ );
+ }
+
+ /* ---- Support Message Center: inbox + live message threads ---- */
+ function MessageCenter() {
+ const seed = D.supportThreads;
+ const [threads, setThreads] = useState(() => seed.map(t => ({ ...t, messages: [...t.messages] })));
+ const [activeId, setActiveId] = useState(seed[0].id);
+ const [input, setInput] = useState('');
+ const [typing, setTyping] = useState(false);
+ const scroller = useRef(null);
+ const active = threads.find(t => t.id === activeId) || threads[0];
+ const totalUnread = threads.reduce((n, t) => n + t.unread, 0);
+ useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [active.messages, typing, activeId]);
+
+ const openThread = (id) => { setActiveId(id); setThreads(ts => ts.map(t => t.id === id ? { ...t, unread: 0 } : t)); };
+ const send = (text) => {
+ const msg = (text || '').trim(); if (!msg) return;
+ setThreads(ts => ts.map(t => t.id === activeId ? { ...t, time: 'Just now', messages: [...t.messages, { from: 'me', text: msg, time: 'Just now' }] } : t));
+ setInput(''); setTyping(true);
+ setTimeout(() => {
+ setTyping(false);
+ setThreads(ts => ts.map(t => t.id === activeId
+ ? { ...t, time: 'Just now', messages: [...t.messages, { from: 'agent', text: `Thank you for your message. ${t.agent.name.split(' ')[0]} from the ${t.agent.role} will review this and get back to you shortly.`, time: 'Just now' }] }
+ : t));
+ }, 1000);
+ };
+
+ const trunc = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' };
+
+ return (
+
+
+
+
+
+
Message Center
+
Secure messaging with YEIDA support officers
+
+
+
Support online
+
+
+ {/* Inbox list */}
+
+
+ Inbox
+ {totalUnread > 0 && {totalUnread} new }
+
+ {threads.map(t => {
+ const last = t.messages[t.messages.length - 1];
+ const on = t.id === activeId;
+ return (
+
openThread(t.id)} style={{ display: 'flex', gap: 11, width: '100%', textAlign: 'left', padding: '13px 16px', border: 'none', borderBottom: '1px solid var(--line-2)', borderLeft: on ? '3px solid var(--blue-500)' : '3px solid transparent', background: on ? 'var(--blue-50)' : '#fff', cursor: 'pointer' }}>
+
+
+ {t.agent.online && }
+
+
+ {t.agent.name} {t.time}
+ {t.subject}
+ {last.from === 'me' ? 'You: ' : ''}{last.text}
+
+ {t.unread > 0 && {t.unread} }
+
+ );
+ })}
+
+ {/* Active thread */}
+
+
+
+
+
+
{active.agent.name}
+
{active.agent.role} · {active.agent.online ? 'Online now' : 'Offline'}
+
+
+
{active.id}
+
+
+ {active.messages.map((m, i) => (
+
+ {m.from === 'agent' &&
}
+
+
+ ))}
+ {typing && (
+
+ {[0, 1, 2].map(i => )}
+
+ )}
+
+
+
+
+ setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Type your message…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 13.5 }} />
+ send(input)}>
+
+
+
+
+
+ );
+ }
+
+ /* Conditional new-ticket form — title, description, screenshot & document upload,
+ with AI vs human routing and a "Talk to AI instead" shortcut. */
+ function NewTicketForm({ onClose, onSubmit, onTalkAI }) {
+ const [title, setTitle] = useState('');
+ const [desc, setDesc] = useState('');
+ const [shot, setShot] = useState('');
+ const [doc, setDoc] = useState('');
+ const [route, setRoute] = useState('human'); // human | ai
+ const shotRef = useRef(null); const docRef = useRef(null);
+ const valid = title.trim() && desc.trim();
+ return (
+
+
+
Raise a support ticket
Tell us what's wrong — attach a screenshot or document if it helps
+
+
+
+
+
Description setDesc(e.target.value)} placeholder="Describe the issue in detail" rows={4} style={{ resize: 'vertical', lineHeight: 1.5, paddingTop: 10 }} />
+
+
Route to
+
+ {[['human', 'Human support', 'user'], ['ai', 'AI assistant', 'shield']].map(([k, l, ic]) => (
+ setRoute(k)} className="row gap-2" style={{ flex: 1, justifyContent: 'center', padding: '10px', borderRadius: 10, cursor: 'pointer', fontWeight: 600, fontSize: 13, border: '1.5px solid ' + (route === k ? 'var(--blue-400)' : 'var(--line)'), background: route === k ? 'var(--blue-50)' : '#fff', color: route === k ? 'var(--blue-700)' : 'var(--ink-2)' }}> {l}
+ ))}
+
+
+
+
Talk to AI instead
+
+ Cancel
+ onSubmit({ title, desc, route })}>Submit ticket
+
+
+
+
+ );
+ }
+
+ /* Callback / email-support request — opens as a popup (modal). Collects name, phone
+ & email, then shows a thank-you confirmation. Used by "Live Call" & "Email Support". */
+ function CallbackForm({ kind, onClose }) {
+ const isCall = kind === 'call';
+ const [done, setDone] = useState(false);
+ const [name, setName] = useState(D.user.name);
+ const [phone, setPhone] = useState('');
+ const [email, setEmail] = useState(D.user.email);
+ const [note, setNote] = useState('');
+ const valid = name.trim() && (isCall ? phone.replace(/\D/g, '').length >= 8 : emailRe.test(email));
+
+ if (done) {
+ return (
+ Done }>
+
+
+
Thank you!
+
Our team will contact you shortly{isCall ? ` on ${phone}` : ` at ${email}`}. You'll also receive a confirmation by email.
+
+
+ );
+ }
+ return (
+
+ Cancel
+ setDone(true)}>{isCall ? 'Request call back' : 'Send request'}
+ >}>
+
+
+
+
How can we help? (optional) setNote(e.target.value)} rows={3} placeholder="Briefly describe your query" style={{ resize: 'vertical', lineHeight: 1.5, paddingTop: 10 }} />
+
+
+ );
+ }
+
+ /* ---------------- SUPPORT ---------------- */
+ function Support({ go, toast = () => {} }) {
+ const [showTicket, setShowTicket] = useState(false);
+ const [request, setRequest] = useState(null); // 'call' | 'email' | null
+ const talkAI = () => { setShowTicket(false); go ? go('assistant') : toast('Connecting you to AI support…'); };
+ const channels = [
+ ['Live Chat', 'Online · avg. 2 min', 'chat', 'emerald', () => go ? go('assistant') : toast('Opening live chat…')],
+ ['Live Call', 'Available 9 AM – 6 PM', 'phone', 'blue', () => { setShowTicket(false); setRequest('call'); }],
+ ['Email Support', 'support@yeida.gov.in', 'mail', 'navy', () => { setShowTicket(false); setRequest('email'); }],
+ ['New Ticket', 'Title, details & attachments', 'plus', 'amber', () => { setRequest(null); setShowTicket(true); }],
+ ];
+ return (
+
+
+ Talk to AI
+ setShowTicket(true)}> New Ticket
+
+
+ {channels.map(([t, s, ic, c, fn]) => {
+ const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'] }[c];
+ return (
+
+
+ {t}
+ {s}
+
+ );
+ })}
+
+
+ {showTicket &&
setShowTicket(false)} onTalkAI={talkAI} onSubmit={({ route }) => { setShowTicket(false); toast(route === 'ai' ? 'Sent to AI support — you\'ll get an instant reply' : 'Ticket submitted — our team will respond shortly'); }} />}
+ {request && setRequest(null)} />}
+
+ {/* Your support team */}
+
+
+
Your support team.
+ Support online
+
+
+ {D.supportTeam.map(m => (
+
+
+
+
+ {m.online && }
+
+
+
{m.name}{m.ai && AI }
+
{m.role}
+
+
+
{m.desc}
+
m.ai ? talkAI() : toast('Message sent to ' + m.name.split(' ')[0])}> {m.ai ? 'Ask AI' : 'Message'}
+
+ ))}
+
+
+
+
+
+
+
My Tickets
+
+ Ticket Subject Priority Status Updated
+
+ {D.tickets.map(t => (
+
+ {t.id}
+ {t.subject}
+ {t.priority}
+ {t.status}
+ {t.updated}
+
+ ))}
+
+
+
+
+
Frequently Asked
+
+ {['How do I generate an e-Challan?', 'What is the construction approval timeline?', 'How is transfer fee calculated?', 'How do I update my KYC?', 'Where can I find my lease deed?'].map(q => (
+
+ {q}
+
+ ))}
+
+
+
+
+ );
+ }
+
+ /* ---------------- PROFILE: shared helpers ---------------- */
+ // Generic centered modal
+ function Modal({ title, sub, icon = 'settings', onClose, children, footer, width = 520 }) {
+ return (
+
+
e.stopPropagation()} className="anim-pop card" style={{ width, maxWidth: '100%', maxHeight: '88vh', padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
+
+
{children}
+ {footer &&
{footer}
}
+
+
+ );
+ }
+
+ // A single OTP entry row — type 6 digits, "verify" the code 000000 (demo) to mark verified
+ function OtpField({ label, sent, value, onChange, verified, onVerify }) {
+ const full = value.length === 6;
+ return (
+
+
{label}
+
+
+ onChange(e.target.value.replace(/\D/g, '').slice(0, 6))}
+ style={verified ? { borderColor: '#7fd3ad', background: 'var(--emerald-50)' } : undefined} />
+ {verified && }
+
+ {!verified &&
Verify }
+
+
{verified ? 'Verified ✓' : `OTP sent to ${sent} · enter 000000 to verify (demo)`}
+
+ );
+ }
+
+ // Change email / mobile.
+ // • EMAIL : identity is always confirmed by an OTP sent to the REGISTERED MOBILE
+ // (not the old email), then the NEW email is verified. Confirming the
+ // OLD email too is governed by authority policy (system-configurable).
+ // • MOBILE: the OLD number is verified, then the NEW number — unless access to the
+ // old number is lost.
+ function ChangeContactModal({ kind, current, registeredMobile, verifyOldEmailPolicy = false, onClose, onSaved }) {
+ const isEmail = kind === 'email';
+ const [val, setVal] = useState('');
+ const [lost, setLost] = useState(false);
+ const [idOtp, setIdOtp] = useState(''); const [idOk, setIdOk] = useState(false); // identity step (mobile OTP for email / old number for phone)
+ const [oldOtp, setOldOtp] = useState(''); const [oldOk, setOldOk] = useState(false); // optional old-email confirmation (policy-driven)
+ const [newOtp, setNewOtp] = useState(''); const [newOk, setNewOk] = useState(false);
+ const reset = () => { setIdOtp(''); setIdOk(false); setOldOtp(''); setOldOk(false); setNewOtp(''); setNewOk(false); };
+ const validNew = isEmail ? emailRe.test(val) : val.replace(/\D/g, '').length >= 8;
+ // Email: identity (mobile) + new email [+ old email if policy on]. Mobile: old number (unless lost) + new number.
+ const canSave = isEmail
+ ? validNew && idOk && newOk && (!verifyOldEmailPolicy || lost || oldOk)
+ : validNew && newOk && (lost || idOk);
+ const verify = (otp, setOk) => otp === '000000' && setOk(true);
+ return (
+
+ Cancel
+ onSaved(val)}>Save changes
+ >}>
+
+
+
+ {/* Email: identity is verified on the registered mobile, NOT the old email */}
+ {isEmail && (
+
+
+ For your security, an OTP to verify this change is sent to your registered mobile {registeredMobile} — not your current email.
+
+ )}
+
+
New {isEmail ? 'email' : 'mobile'}
+
+ { setVal(e.target.value); reset(); }} placeholder={isEmail ? 'you@email.com' : 'New number'} inputMode={isEmail ? 'email' : 'tel'} />
+
+ {val.length > 0 && !validNew &&
Enter a valid {isEmail ? 'email' : 'mobile number'}}
+
+
+
+ { setLost(e.target.checked); reset(); }} style={{ width: 16, height: 16, accentColor: 'var(--blue-600)' }} />
+ I lost access to my old {isEmail ? 'email' : 'phone'} (got a new {isEmail ? 'address' : 'number'})
+
+
+ {validNew && (
+
+ {isEmail ? (
+ <>
+ {/* Step 1 — identity via registered mobile (always required) */}
+
verify(idOtp, setIdOk)} />
+ {/* Step 2 — old-email confirmation only when authority policy requires it */}
+ {verifyOldEmailPolicy && !lost && verify(oldOtp, setOldOk)} />}
+ {/* Step 3 — verify the new email */}
+ verify(newOtp, setNewOk)} />
+ {lost && No access to your old email — that's fine, identity is confirmed via your registered mobile. The change is logged for review.
}
+ {!verifyOldEmailPolicy && Old-email confirmation is currently not required (system-configurable by the Authority).
}
+ >
+ ) : (
+ <>
+ {!lost && verify(idOtp, setIdOk)} />}
+ verify(newOtp, setNewOk)} />
+ {lost && Lost-access flow — only the new number is verified. The change is logged for review.
}
+ >
+ )}
+
+ )}
+
+
+ );
+ }
+
+ function ChangePasswordModal({ onClose, onSaved }) {
+ const [cur, setCur] = useState(''); const [nw, setNw] = useState(''); const [cf, setCf] = useState('');
+ const short = nw.length > 0 && nw.length < 8;
+ const mismatch = cf.length > 0 && nw !== cf;
+ const ok = cur.length > 0 && nw.length >= 8 && nw === cf;
+ return (
+
+ Cancel
+ Update password
+ >}>
+
+
+
New password setNw(e.target.value)} placeholder="Min 8 characters" style={short ? { borderColor: 'var(--red-500)' } : undefined} />
{short &&
At least 8 characters}
+
Confirm new password setCf(e.target.value)} style={mismatch ? { borderColor: 'var(--red-500)' } : undefined} />
{mismatch &&
Passwords do not match}
+
+
+ );
+ }
+
+ // Two-Factor: a change is only allowed when the CURRENT PASSWORD is known AND an
+ // OTP/TP is verified — otherwise the change stays blocked.
+ function TwoFAModal({ method, onClose, onSaved }) {
+ const [m, setM] = useState(method);
+ const [pw, setPw] = useState('');
+ const [otp, setOtp] = useState(''); const [otpOk, setOtpOk] = useState(false);
+ const opts = [['sms', 'SMS OTP', 'OTP sent to your primary mobile', 'phone'], ['app', 'Authenticator App', 'Use Google / Microsoft Authenticator', 'shield'], ['email', 'Email OTP', 'OTP sent to your primary email', 'mail']];
+ const canSave = pw.length > 0 && otpOk;
+ return (
+
+ Cancel
+ onSaved(m)}>Save method
+ >}>
+
+ {opts.map(([k, l, s, ic]) => (
+
setM(k)} className="row spread" style={{ width: '100%', textAlign: 'left', padding: '13px 14px', borderRadius: 12, cursor: 'pointer', border: '1.5px solid ' + (m === k ? 'var(--blue-400)' : 'var(--line)'), background: m === k ? 'var(--blue-50)' : '#fff' }}>
+ {l} {s}
+ {m === k && }
+
+ ))}
+ {m === 'app' &&
Scan with your authenticator app }
+
+ {/* Required: current password + OTP/TP to authorise the change */}
+
+
Authorise this change
+
+
otp === '000000' && setOtpOk(true)} />
+ {!canSave && Change stays blocked until your current password is entered and the OTP is verified.
}
+
+
+
+ );
+ }
+
+ /* ---------------- PROFILE ---------------- */
+ function Profile({ go }) {
+ const [toastNode, toast] = useToast();
+ const [tab, setTab] = useState('personal');
+ const [photo, setPhoto] = useUserPhoto();
+ const user = useUser(D.user);
+ const fileRef = useRef(null);
+ const tabs = [['personal', 'Personal Info', 'user'], ['kyc', 'KYC Verification', 'shield'], ['security', 'Security', 'lock'], ['notifications', 'Notifications', 'bell'], ['devices', 'Devices', 'device']];
+
+ const onPhoto = (e) => {
+ const file = e.target.files && e.target.files[0];
+ if (file) readImageFile(file, (url) => { setPhoto(url); toast('Profile photo updated'); });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {photo
+ ?
+ :
}
+
fileRef.current && fileRef.current.click()} title="Upload photo" style={{ position: 'absolute', bottom: 2, right: 2, width: 28, height: 28, borderRadius: 99, border: '2px solid #fff', background: 'var(--blue-600)', color: '#fff', display: 'grid', placeItems: 'center', cursor: 'pointer', boxShadow: 'var(--sh-sm)' }}>
+
+
+
{user.name} {user.isAllottee ? 'Allottee' : user.relationship + ' · on behalf of allottee'} · {D.user.allotment}
+
+
+ KYC Verified
+ Member since {D.user.memberSince}
+
+
+
+
+
+
+ {tabs.map(([k, l, ic]) => (
+ setTab(k)} className="row gap-3" style={{ width: '100%', padding: '11px 13px', borderRadius: 10, border: 'none', background: tab === k ? 'var(--blue-50)' : 'transparent', color: tab === k ? 'var(--blue-600)' : 'var(--ink-2)', fontWeight: tab === k ? 700 : 500, fontSize: 14, cursor: 'pointer', textAlign: 'left' }}>
+ {l}
+
+ ))}
+
+
+ {tab === 'personal' &&
}
+ {tab === 'kyc' &&
}
+ {tab === 'security' &&
}
+ {tab === 'notifications' &&
}
+ {tab === 'devices' &&
}
+
+
+ {toastNode}
+
+ );
+ }
+
+ // Editable name + locked allotment identity + change-with-OTP for email/mobile
+ function PersonalTab({ toast }) {
+ const u = useUser(D.user);
+ const [edit, setEdit] = useState(false);
+ const [first, setFirst] = useState(u.firstName);
+ const [last, setLast] = useState(u.lastName);
+ const [email, setEmail] = useState(u.email);
+ const [mobile, setMobile] = useState(D.user.mobile);
+ const [change, setChange] = useState(null); // 'email' | 'phone' | null
+ // The allottee can be a different person from the account holder (e.g. a spouse).
+ const [relationship, setRelationship] = useState(u.relationship);
+ const [allotteeFirst, setAllotteeFirst] = useState(u.allotteeFirstName);
+ const [allotteeLast, setAllotteeLast] = useState(u.allotteeLastName);
+ const isSelf = relationship === 'Self';
+
+ const locked = [['Allotment Number', D.user.allotment, 'building'], ['Sector', D.user.sector, 'map'], ['Plot Number', D.user.plot, 'plot'], ['Pocket', D.user.pocket, 'grid']];
+
+ const save = () => {
+ setUserProfile({
+ name: `${first} ${last}`.trim(), firstName: first, lastName: last,
+ initials: ((first[0] || '') + (last[0] || '')).toUpperCase(), email,
+ relationship, isAllottee: isSelf,
+ allotteeFirstName: isSelf ? first : allotteeFirst,
+ allotteeLastName: isSelf ? last : allotteeLast,
+ });
+ setEdit(false); toast('Personal information saved');
+ };
+
+ return (
+
+
+
Personal Information
+ {edit
+ ?
setEdit(false)}>Cancel Save
+ :
setEdit(true)}> Edit }
+
+
These are the account holder's details. Your name and contact (email & mobile) are editable — the allottee may be a different person (see below). Allotment identity fields are locked to authority records after verification.
+
+ {/* Account holder — editable name + change-with-verification contact */}
+
Account Holder
+
+
+
+
+
Email Address
+
+
setChange('email')}> Change with verification
+
+
Mobile Number
+
+
setChange('phone')}> Change with verification
+
+
+
+ {/* Allottee — may differ from the account holder (e.g. spouse / family member) */}
+
Allottee Details
+
The allottee may be someone other than the account holder — for example, the plot is in your spouse's name while you manage the account. The email and mobile above always belong to the account holder.
+
+
Relationship to Allottee
+ setRelationship(e.target.value)} style={!edit ? { background: 'var(--bg)', cursor: 'not-allowed' } : undefined}>
+ {D.relationshipOptions.map(o => {o} )}
+
+
+ {isSelf ? (
+
Allottee Name
+
+
You are the original allottee (same as account holder).
+
+ ) : (
+
+ )}
+
+ {!isSelf && (
+
+
+
+
Allottee ≠ account holder — registered as {relationship} .
+
+
+ )}
+
+ {/* Locked allotment identity */}
+
Allotment Identity Locked after verification
+
+ {locked.map(([l, v, ic]) => (
+
+ ))}
+
+
+ {change && (
+
setChange(null)}
+ onSaved={(val) => { if (change === 'email') { setEmail(val); setUserProfile({ email: val }); } else setMobile(val); setChange(null); toast((change === 'email' ? 'Email' : 'Mobile') + ' updated & verified'); }} />
+ )}
+
+ );
+ }
+
+ // Inline KYC upload panel — shown directly on the KYC screen (NOT a popup).
+ function KycUploadPanel({ type, setType, fileName, setFileName, onUpload, panelRef }) {
+ const fileRef = useRef(null);
+ const meta = D.kycDocTypes.find(t => t.id === type) || {};
+ const typeIcon = (t) => t.proof === 'photo' ? 'user' : t.proof === 'address' ? 'mapPin' : 'file';
+ return (
+
+
+
+
Upload a document
Pick the document type, then choose a file (JPG, PNG or PDF)
+
+
Document type
+
+ {D.kycDocTypes.map(t => {
+ const on = type === t.id;
+ return (
+ setType(t.id)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, padding: '12px 8px', borderRadius: 12, cursor: 'pointer', textAlign: 'center', border: '1.5px solid ' + (on ? 'var(--blue-400)' : 'var(--line)'), background: on ? 'var(--blue-50)' : '#fff', boxShadow: on ? '0 0 0 3px rgba(38,112,230,.1)' : 'none' }}>
+
+ {t.label}
+
+ );
+ })}
+
+
{meta.hint}
+
{ const f = e.target.files && e.target.files[0]; if (f) setFileName(f.name); }} />
+
+ fileRef.current && fileRef.current.click()}> {fileName || 'Choose file'}
+ Upload document
+
+ {fileName &&
{fileName} selected}
+
+ );
+ }
+
+ function KycTab({ toast }) {
+ const K = D.kyc;
+ const [docs, setDocs] = useState(K.documents);
+ const [docType, setDocType] = useState('utility');
+ const [fileName, setFileName] = useState('');
+ const uploadRef = useRef(null);
+ const focusUpload = (t) => { if (t) setDocType(t); if (uploadRef.current) uploadRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); };
+ const typeLabel = (id) => (D.kycDocTypes.find(t => t.id === id) || {}).label || id;
+ const proofLabel = (proof) => proof === 'photo' ? 'Photograph' : proof === 'address' ? 'Address proof' : proof.includes('address') ? 'ID + address proof' : 'Identity proof';
+ const onUpload = () => {
+ if (!fileName) return;
+ const m = D.kycDocTypes.find(t => t.id === docType) || {};
+ const proof = (m.proof === 'identity' && m.alsoAddress) ? 'identity+address' : m.proof;
+ setDocs(d => [...d, { type: docType, number: fileName, proof, status: 'pending', expiry: m.expires ? 'To be read from document' : null }]);
+ setFileName('');
+ toast(typeLabel(docType) + ' uploaded — pending verification');
+ };
+
+ // Required proof categories — drive the "what's still pending" checklist
+ const REQUIREMENTS = [
+ { key: 'identity', label: 'Identity Proof', desc: 'Aadhaar, PAN, Passport or Driving Licence', icon: 'shield', uploadType: 'aadhaar', match: d => d.proof.includes('identity') },
+ { key: 'address', label: 'Address Proof', desc: 'Aadhaar, Passport, DL or Electricity / Utility bill', icon: 'mapPin', uploadType: 'utility', match: d => d.proof.includes('address') },
+ { key: 'photo', label: 'Photograph', desc: 'Recent passport-size photograph', icon: 'user', uploadType: 'photo', match: d => d.proof === 'photo' },
+ ];
+ const reqStatus = (r) => {
+ const ms = docs.filter(r.match);
+ if (ms.some(d => d.status === 'verified')) return 'verified';
+ if (ms.length) return 'pending';
+ return 'missing';
+ };
+ const verifiedCount = REQUIREMENTS.filter(r => reqStatus(r) === 'verified').length;
+ const pending = REQUIREMENTS.filter(r => reqStatus(r) !== 'verified');
+ const pct = Math.round((verifiedCount / REQUIREMENTS.length) * 100);
+ // Show pending / not-uploaded first, completed (verified) last
+ const rank = { missing: 0, pending: 1, verified: 2 };
+ const sortedReqs = [...REQUIREMENTS].sort((a, b) => rank[reqStatus(a)] - rank[reqStatus(b)]);
+
+ const STAT = {
+ verified: { tone: 'green', icon: 'check', label: 'Verified' },
+ pending: { tone: 'amber', icon: 'clock', label: 'Pending review' },
+ missing: { tone: 'gray', icon: 'alert', label: 'Not uploaded' },
+ };
+
+ return (
+
+
+
KYC Verification
+ focusUpload()}> Upload document
+
+
KYC is collected once you become a customer — after the allotment contract is signed. Upload identity proof, address proof (including an electricity/utility bill) and a photograph.
+
+ {/* Completion summary */}
+
+
+
+
+
+
{pending.length ? `${pending.length} item${pending.length > 1 ? 's' : ''} still pending` : 'All KYC requirements complete'}
+
{verifiedCount} of {REQUIREMENTS.length} verified{K.status === 'verified' ? ` · last verified ${K.verifiedOn}` : ''}
+
+
+
{pct}%
+
+
+
+
+ {K.viaPOA &&
Contract signed by the allottee; KYC completed for the representative via Power of Attorney.
}
+
+ {/* Inline upload panel — always on screen (no popup) */}
+
+
+ {/* Requirements checklist — pending / not-uploaded shown first, completed last */}
+
+
Required documents
+ {pending.length > 0 &&
{pending.length} pending on top}
+
+
+ {sortedReqs.map(r => {
+ const st = reqStatus(r); const s = STAT[st];
+ const owned = docs.filter(r.match);
+ return (
+
+
+
+
+
{r.label}
+
{st === 'verified' && owned.length ? `On file: ${typeLabel(owned.find(d => d.status === 'verified').type)}` : r.desc}
+
+
+
+ {s.label}
+ {st !== 'verified' && focusUpload(r.uploadType)}> {st === 'missing' ? 'Upload' : 'Re-upload'} }
+
+
+ );
+ })}
+
+
+ {/* Documents on file */}
+
+
Documents on file ({docs.length})
+
focusUpload()}> Add / update
+
+ {docs.length === 0
+ ?
No documents uploaded yet
+ : [...docs].sort((a, b) => (a.status === 'verified' ? 1 : 0) - (b.status === 'verified' ? 1 : 0)).map((doc, di) => {
+ const meta = D.kycDocTypes.find(t => t.id === doc.type) || {};
+ return (
+
+
+
+
+
{typeLabel(doc.type)}
+ {proofLabel(doc.proof)}
+
+
{doc.number}
+ {meta.expires ? · Expires {doc.expiry} : doc.proof === 'photo' ? null : · No expiry }
+
+
+
+
{doc.status === 'verified' ? 'Verified' : 'Pending'}
+
+ );
+ })}
+
+ {/* Acceptable document reference */}
+
+
Acceptable documents
+
+ {D.kycDocTypes.map(t => (
+
+
+ {t.label} — {t.hint}
+
+ ))}
+
+
+
+ );
+ }
+
+ function SecurityTab({ toast }) {
+ const [modal, setModal] = useState(null); // 'pw' | '2fa' | null
+ const [twoFa, setTwoFa] = useState('sms');
+ const [loginAlerts, setLoginAlerts] = useState(true);
+ const twoFaLabel = { sms: 'OTP via mobile', app: 'Authenticator app', email: 'OTP via email' }[twoFa];
+ return (
+
+
Security Settings
+
+ {[['Password', 'Last changed 3 months ago', 'lock', () => setModal('pw'), 'Change'],
+ ['Two-Factor Authentication', twoFaLabel + ' enabled', 'shield', () => setModal('2fa'), 'Manage']].map(([l, s, ic, fn, b]) => (
+
+ ))}
+
+ {/* Login alerts toggle */}
+
+
Login Alerts
Email me on every new-device login
+
{ setLoginAlerts(v); toast('Login alerts ' + (v ? 'enabled' : 'disabled')); }} />
+
+
+ {modal === 'pw' &&
setModal(null)} onSaved={() => { setModal(null); toast('Password updated'); }} />}
+ {modal === '2fa' && setModal(null)} onSaved={(m) => { setTwoFa(m); setModal(null); toast('2FA method saved'); }} />}
+
+ );
+ }
+
+ // Verify a single unverified contact via OTP (demo: enter 000000)
+ function VerifyContactModal({ kind, value, onClose, onVerified }) {
+ const isEmail = kind === 'email';
+ const [otp, setOtp] = useState(''); const [ok, setOk] = useState(false);
+ return (
+
+ Cancel
+ Done
+ >}>
+ otp === '000000' && setOk(true)} />
+
+ );
+ }
+
+ function NotifTab({ toast }) {
+ const [emails, setEmails] = useState(D.contactPrefs.emails);
+ const [phones, setPhones] = useState(D.contactPrefs.phones);
+ const [rem, setRem] = useState(D.contactPrefs.reminders);
+ const [mkt, setMkt] = useState(D.contactPrefs.marketing);
+ const [verify, setVerify] = useState(null); // { kind, id, value }
+ const [adding, setAdding] = useState(null); // 'email' | 'phone' | null
+ const [draft, setDraft] = useState('');
+ const trunc = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' };
+
+ const makePrimary = (kind, id) => {
+ const upd = (list) => list.map(x => ({ ...x, primary: x.id === id }));
+ kind === 'email' ? setEmails(upd) : setPhones(upd);
+ toast('Primary ' + kind + ' updated');
+ };
+ const markVerified = (kind, id) => {
+ const upd = (list) => list.map(x => x.id === id ? { ...x, verified: true } : x);
+ kind === 'email' ? setEmails(upd) : setPhones(upd);
+ };
+ const addContact = (kind) => {
+ const v = draft.trim(); if (!v) return;
+ const item = { id: (kind === 'email' ? 'e' : 'p') + '-' + (emails.length + phones.length + 1), value: v, primary: false, verified: false };
+ kind === 'email' ? setEmails(l => [...l, item]) : setPhones(l => [...l, item]);
+ setAdding(null); setDraft(''); toast('Secondary ' + kind + ' added — please verify');
+ };
+
+ // reminders: five channels, each routes to a chosen contact (post → mailing address);
+ // at least one channel must stay on
+ const CHANNELS = ['email', 'sms', 'whatsapp', 'call', 'post'];
+ const enabledCount = CHANNELS.filter(k => rem[k].on).length;
+ const setChannel = (k, patch) => setRem(r => ({ ...r, [k]: { ...r[k], ...patch } }));
+ const toggleChannel = (k) => {
+ if (rem[k].on && enabledCount <= 1) { toast('At least one notification channel must stay on'); return; }
+ setChannel(k, { on: !rem[k].on });
+ };
+ const mail = D.user.mailingAddress;
+
+ // plain render helpers (NOT nested components) so inputs keep focus across re-renders
+ const renderContacts = (kind, title, icon, list) => (
+
+
+
{title}
+
{ setAdding(kind); setDraft(''); }}> Add
+
+ {list.map(c => (
+
+
+ makePrimary(kind, c.id)} style={{ width: 16, height: 16, accentColor: 'var(--blue-600)', flex: 'none' }} />
+ {c.value}
+
+
+ {c.owner === 'allottee' && Allottee }
+ {c.primary ? 'Primary' : 'Secondary'}
+ {c.verified
+ ? Verified
+ : setVerify({ kind, id: c.id, value: c.value })}>Verify }
+
+
+ ))}
+ {adding === kind && (
+
+
setDraft(e.target.value)} placeholder={kind === 'email' ? 'new@email.com' : '+91 9xxxxxxxxx'} inputMode={kind === 'email' ? 'email' : 'tel'} autoFocus />
+
addContact(kind)}>Add
+
{ setAdding(null); setDraft(''); }}>Cancel
+
+ )}
+
+ );
+
+ const channelSub = { email: 'Routed to the chosen email', sms: 'SMS to the chosen mobile', whatsapp: 'WhatsApp to the chosen mobile', call: 'Automated call to the chosen mobile', post: 'Letter sent to your mailing address' };
+ const renderChannel = (k, label, icon, slots) => (
+
+
+ {k === 'post'
+ ?
{mail.city} — {mail.pin}
+ :
setChannel(k, { to: e.target.value })}>
+ {slots.map(s => {s.value}{s.primary ? ' (Primary)' : ''}{s.owner === 'allottee' ? ' — Allottee' : ''} )}
+ }
+
toggleChannel(k)} title={rem[k].on ? 'Turn off' : 'Turn on'} style={{ width: 46, height: 26, borderRadius: 99, border: 'none', cursor: 'pointer', background: rem[k].on ? 'var(--emerald-500)' : '#cdd7e4', position: 'relative', flex: 'none' }}>
+
+
+
+ );
+
+ return (
+
+
Notifications & Contact
+
Manage your primary & secondary email and mobile. The allottee may be a different person (e.g. living abroad) — their contact is tagged Allottee , and any payment-reminder channel below can be routed to it. At least one channel must stay enabled.
+
+ {renderContacts('email', 'Email addresses', 'mail', emails)}
+ {renderContacts('phone', 'Mobile numbers', 'phone', phones)}
+
+
+
Payment reminders & notifications
+
{enabledCount} of 5 channels on · at least one required
+
+ {renderChannel('email', 'Email', 'mail', emails)}
+ {renderChannel('sms', 'SMS', 'phone', phones)}
+ {renderChannel('whatsapp', 'WhatsApp', 'messages', phones)}
+ {renderChannel('call', 'Phone call', 'phone', phones)}
+ {renderChannel('post', 'Postal mail', 'mail', null)}
+
+
Newsletter & marketing
+
setMkt(m => ({ ...m, newsletter: v }))} />
+ setMkt(m => ({ ...m, promotions: v }))} />
+ setMkt(m => ({ ...m, productUpdates: v }))} />
+
+ {verify && setVerify(null)} onVerified={() => { markVerified(verify.kind, verify.id); setVerify(null); toast((verify.kind === 'email' ? 'Email' : 'Mobile') + ' verified'); }} />}
+
+ );
+ }
+
+ function Toggle({ label, on, onChange }) {
+ const [v, setV] = useState(on);
+ const toggle = () => { const nv = !v; setV(nv); onChange && onChange(nv); };
+ return (
+
+ {label && {label} }
+
+
+
+
+ );
+ }
+
+ function DevicesTab({ toast }) {
+ const dev = [['MacBook Pro · Chrome', 'Greater Noida · Active now', true], ['iPhone 14 · YEIDA App', 'Greater Noida · 2h ago', false], ['Windows PC · Edge', 'Delhi · 5 days ago', false]];
+ return (
+
+
Connected Devices
+ {dev.map(([l, s, cur], i) => (
+
+
+ {cur ?
This device :
toast('Signed out of device')}>Sign out }
+
+ ))}
+
+ {/* Activity log */}
+
Recent Activity
+
+ {D.loginActivity.map((a, i, arr) => {
+ const cc = a.ok ? 'var(--emerald-500)' : 'var(--red-500)';
+ return (
+
+ {i !== arr.length - 1 &&
}
+
+
+
{a.action}
+
{a.device} · {a.location}
+
IP {a.ip} · {a.time}
+
+
+ );
+ })}
+
+
+ );
+ }
+
+export { Notices, SectorMaps, Support, Profile };
diff --git a/src/components/portal/floating-assistant.jsx b/src/components/portal/floating-assistant.jsx
new file mode 100644
index 0000000..8e09ba8
--- /dev/null
+++ b/src/components/portal/floating-assistant.jsx
@@ -0,0 +1,89 @@
+'use client';
+/* Floating AI Assistant — global launcher + compact chat panel, on every app page */
+import React, { useState, useRef, useEffect } from 'react';
+import Icon from './icons';
+
+ // lightweight reply engine (shares the spirit of the full-page assistant)
+ function reply(text) {
+ const t = text.toLowerCase();
+ const A = [
+ { m: ['construction', 'build', 'start'], r: "Your construction is at Step 4 — Authority Review. Next action: upload the pending Soil Test Report. Want me to open the Construction module?", a: ['construction', 'Open Construction'] },
+ { m: ['payment', 'due', 'pay', 'challan'], r: "You have ₹1,47,585 across 3 dues. Authority records and challans are on your Payments page.", a: ['payments', 'Open Payments'] },
+ { m: ['document', 'paper', 'lease', 'atl'], r: "Lease Deed: Completed. ATL: Pending — that's your next paperwork item. All documents live in the Document Center.", a: ['documents', 'Document Center'] },
+ { m: ['sell', 'transfer'], r: "To sell Plot B-47 you're eligible (7 years held). Estimated transfer cost ≈ ₹2.02 L. Shall I open the Transfer module?", a: ['transfer', 'Start Transfer'] },
+ { m: ['plan', 'design', 'tour', 'sample'], r: "I've matched 3 plans to your 300 sq.m east-facing plot, with 3D walkthroughs. Open Sample Plans?", a: ['sampleplans', 'View Plans'] },
+ { m: ['map', 'sector', 'location'], r: "Plot B-47 is in Sector 18, Block B — an east-facing 300 sq.m plot. I can open the interactive map.", a: ['sectormaps', 'Open Map'] },
+ ];
+ const hit = A.find(x => x.m.some(k => t.includes(k)));
+ return hit ? { reply: hit.r, action: hit.a } : { reply: "I can help with your plot status, authority paperwork, payments, construction, sample plans and transfers. What would you like to know?" };
+ }
+
+ function FloatingAssistant({ go }) {
+ const [open, setOpen] = useState(false);
+ const [msgs, setMsgs] = useState([{ role: 'bot', text: "Hi Rajeev 👋 Quick help on your plot — payments, paperwork, construction or plans. Ask away, or open the full assistant." }]);
+ const [input, setInput] = useState('');
+ const [typing, setTyping] = useState(false);
+ const scroller = useRef(null);
+ useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, typing, open]);
+
+ const send = (text) => {
+ if (!text.trim()) return;
+ setMsgs(m => [...m, { role: 'user', text }]); setInput(''); setTyping(true);
+ setTimeout(() => { const r = reply(text); setTyping(false); setMsgs(m => [...m, { role: 'bot', text: r.reply, action: r.action }]); }, 800);
+ };
+
+ const quick = ['What payments are pending?', 'What is my ATL status?', 'Show sample plans'];
+
+ return (
+
+ {open && (
+
+ {/* header */}
+
+
+
+ { setOpen(false); go('assistant'); }} title="Open full assistant" style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', display: 'grid', placeItems: 'center' }}>
+ setOpen(false)} style={{ width: 30, height: 30, borderRadius: 8, border: 'none', background: 'rgba(255,255,255,.12)', color: '#fff', cursor: 'pointer', display: 'grid', placeItems: 'center' }}>
+
+
+ {/* messages */}
+
+ {msgs.map((m, i) => (
+
+ {m.role === 'bot' &&
}
+
+
{m.text}
+ {m.action &&
{ setOpen(false); go(m.action[0]); }}>{m.action[1]} }
+
+
+ ))}
+ {typing &&
{[0, 1, 2].map(i => )}
}
+ {msgs.length === 1 && (
+
+ {quick.map(q => send(q)} style={{ textAlign: 'left', fontSize: 12.5, fontWeight: 600, color: 'var(--ink-2)', padding: '9px 12px', borderRadius: 10, border: '1px solid var(--line)', background: '#fff', cursor: 'pointer' }}>{q} )}
+
+ )}
+
+ {/* input */}
+
+
+ setInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && send(input)} placeholder="Ask anything…" style={{ flex: 1, border: 'none', background: 'none', outline: 'none', fontSize: 13.5 }} />
+ send(input)}>
+
+
+
+ )}
+ {/* launcher */}
+
setOpen(o => !o)} style={{ width: 58, height: 58, borderRadius: 99, border: 'none', cursor: 'pointer', background: open ? 'var(--navy-800)' : 'linear-gradient(150deg,var(--blue-500),var(--navy-800))', color: '#fff', boxShadow: '0 16px 36px -10px rgba(11,37,72,.6)', display: 'grid', placeItems: 'center', transition: 'transform .15s' }} title="AI Assistant">
+
+ {!open && }
+
+
+ );
+ }
+
+export { FloatingAssistant };
+export default FloatingAssistant;
diff --git a/src/components/portal/icons.jsx b/src/components/portal/icons.jsx
new file mode 100644
index 0000000..64629ea
--- /dev/null
+++ b/src/components/portal/icons.jsx
@@ -0,0 +1,103 @@
+/* Icon set — lightweight stroke icons as React components.
+ Usage:
*/
+import React from 'react';
+
+const P = {
+ dashboard: 'M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z',
+ plot: 'M4 4h16v16H4z M4 9h16 M4 14h16 M9 4v16 M14 4v16',
+ construction: 'M3 21h18 M5 21V8l7-4 7 4v13 M9 21v-5h6v5 M9.5 11h.01 M14.5 11h.01',
+ payments: 'M3 7h18v10H3z M3 11h18 M7 15h3',
+ documents: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z M9 13h6 M9 17h6',
+ notices: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0',
+ map: 'M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2Z M9 4v14 M15 6v14',
+ transfer: 'M7 8h14 M7 8l4-4 M7 8l4 4 M17 16H3 M17 16l-4-4 M17 16l-4 4',
+ assistant: 'M12 3a9 9 0 0 0-9 9v5a3 3 0 0 0 3 3h2v-7H5v-1a7 7 0 0 1 14 0v1h-3v7h2a3 3 0 0 0 3-3v-5a9 9 0 0 0-9-9Z',
+ support: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3 M12 17h.01',
+ profile: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z',
+ logout: 'M16 17l5-5-5-5 M21 12H9 M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4',
+ search: 'M21 21l-4.3-4.3 M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z',
+ bell: 'M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9 M13.7 21a2 2 0 0 1-3.4 0',
+ chevR: 'M9 6l6 6-6 6',
+ chevL: 'M15 6l-6 6 6 6',
+ chevD: 'M6 9l6 6 6-6',
+ chevU: 'M6 15l6-6 6 6',
+ arrowR: 'M5 12h14 M13 6l6 6-6 6',
+ check: 'M20 6 9 17l-5-5',
+ checkCircle: 'M22 11.1V12a10 10 0 1 1-5.9-9.1 M22 4 12 14.1l-3-3',
+ x: 'M18 6 6 18 M6 6l12 12',
+ plus: 'M12 5v14 M5 12h14',
+ download: 'M12 3v12 M7 10l5 5 5-5 M5 21h14',
+ upload: 'M12 21V9 M7 14l5-5 5 5 M5 3h14',
+ eye: 'M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
+ bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z',
+ filter: 'M3 5h18 M6 12h12 M10 19h4',
+ clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 7v5l3 2',
+ calendar: 'M8 3v4 M16 3v4 M3 9h18 M5 5h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Z',
+ mapPin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
+ phone: 'M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2 4.2 2 2 0 0 1 4 2h3a2 2 0 0 1 2 1.7c.1.9.4 1.8.7 2.7a2 2 0 0 1-.5 2.1L8 9.6a16 16 0 0 0 6 6l1.1-1.1a2 2 0 0 1 2.1-.5c.9.3 1.8.6 2.7.7A2 2 0 0 1 22 16.9Z',
+ mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z M22 7l-10 6L2 7',
+ lock: 'M5 11h14a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1Z M8 11V7a4 4 0 1 1 8 0v4',
+ shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z M9 12l2 2 4-4',
+ rupee: 'M6 4h12 M6 8h12 M9 4c4 0 6 2 6 5s-2 4-5 4H8l6 7 M6 13h2',
+ building: 'M3 21h18 M5 21V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v17 M15 8h3a1 1 0 0 1 1 1v12 M8 7h3 M8 11h3 M8 15h3',
+ trend: 'M3 17l6-6 4 4 8-8 M21 7v6h-6',
+ alert: 'M12 9v4 M12 17h.01 M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z',
+ info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M12 16v-4 M12 8h.01',
+ send: 'M22 2 11 13 M22 2l-7 20-4-9-9-4 20-7Z',
+ sparkles: 'M12 3l1.9 4.6L18.5 9.5 13.9 11.4 12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3Z M19 14l.8 2L22 16.8 20 17.6 19 20l-.8-2.4L16 16.8l2.2-.8L19 14Z',
+ menu: 'M3 6h18 M3 12h18 M3 18h18',
+ moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z',
+ globe: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z M2 12h20 M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20Z',
+ grid: 'M3 3h7v7H3z M14 3h7v7h-7z M14 14h7v7h-7z M3 14h7v7H3z',
+ layers: 'M12 2 2 7l10 5 10-5-10-5Z M2 17l10 5 10-5 M2 12l10 5 10-5',
+ zoomIn: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M11 8v6 M8 11h6',
+ zoomOut: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z M21 21l-4.3-4.3 M8 11h6',
+ file: 'M14 3v5h5 M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-5Z',
+ receipt: 'M5 3h14v18l-3-2-2 2-2-2-2 2-2-2-3 2V3Z M9 8h6 M9 12h6',
+ edit: 'M12 20h9 M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5Z',
+ star: 'M12 3l2.9 6 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.8 3 1.1-6.5L2.6 9.9 9 9l3-6Z',
+ user: 'M20 21a8 8 0 1 0-16 0 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z',
+ settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z M19.4 15a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 1 1-4 0v-.1A1.6 1.6 0 0 0 7 19.4a1.6 1.6 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0-1.1-2.7H1a2 2 0 1 1 0-4h.1A1.6 1.6 0 0 0 2.6 7a1.6 1.6 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 7 2.6 1.6 1.6 0 0 0 8 1.1V1a2 2 0 1 1 4 0v.1A1.6 1.6 0 0 0 14.9 2.6a1.6 1.6 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0 1.1 2.7H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z',
+ history: 'M3 3v6h6 M3.5 9a9 9 0 1 0 2-5L3 9 M12 7v5l3 2',
+ home: 'M3 11l9-8 9 8 M5 9v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9',
+ key: 'M15 7a4 4 0 1 1-5.7 3.6L3 17v3h3l1-1h2v-2h2v-2l1.3-1.3A4 4 0 0 1 15 7Z M16.5 7.5h.01',
+ device: 'M4 4h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8 20h8 M12 16v4',
+ paperclip: 'M21 11.5 12 20a5 5 0 0 1-7-7l9-8.5a3.3 3.3 0 0 1 4.7 4.7L9.6 17.7a1.7 1.7 0 0 1-2.3-2.3l8-7.6',
+ ticket: 'M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2 2 2 0 0 0 0 6 2 2 0 0 1-2 2H5a2 2 0 0 1-2-2 2 2 0 0 0 0-6Z M13 7v10',
+ chat: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z',
+ flag: 'M4 22V4 M4 4h13l-2 4 2 4H4',
+ refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v5h-5',
+ bed: 'M3 7v11 M3 12h18v6 M21 18v-5a3 3 0 0 0-3-3h-7v5 M7 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
+ bath: 'M4 12V6a2 2 0 0 1 2-2 2 2 0 0 1 2 2 M3 12h18v3a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4v-3Z M6 19l-1 2 M18 19l1 2 M8 6h.01',
+ ruler: 'M3 9l6-6 12 12-6 6L3 9Z M9 5l1.5 1.5 M12 8l1.5 1.5 M15 11l1.5 1.5 M6 12l1.5 1.5',
+ briefcase: 'M3 8h18v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8Z M8 8V6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 M3 13h18',
+ users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z M22 21v-2a4 4 0 0 0-3-3.9 M16 3.1a4 4 0 0 1 0 7.8',
+ tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V3h10l7.6 7.6a2 2 0 0 1 0 2.8Z M7.5 7.5h.01',
+ image: 'M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z M21 16l-5-5L5 21',
+ dots: 'M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z M12 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z',
+ megaphone: 'M3 11v2a1 1 0 0 0 1 1h2l5 4V6L6 10H4a1 1 0 0 0-1 1Z M16 8a4 4 0 0 1 0 8 M19 5a8 8 0 0 1 0 14',
+ arrowUpRight: 'M7 17 17 7 M8 7h9v9',
+ arrowDownRight: 'M7 7l10 10 M17 8v9H8',
+ heart: 'M20.8 5.6a5 5 0 0 0-7.1 0L12 7.3l-1.7-1.7a5 5 0 1 0-7.1 7.1L12 21.5l8.8-8.8a5 5 0 0 0 0-7.1Z',
+ messages: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 9 9 0 0 1-4-1L3 21l1.2-4.8A8.4 8.4 0 0 1 12 3a8.4 8.4 0 0 1 9 8.5Z',
+ sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10Z M12 1v2 M12 21v2 M4.2 4.2l1.4 1.4 M18.4 18.4l1.4 1.4 M1 12h2 M21 12h2 M4.2 19.8l1.4-1.4 M18.4 5.6l1.4-1.4',
+ chartBar: 'M3 3v18h18 M8 17v-5 M13 17V8 M18 17v-9',
+ funnel: 'M3 5h18 M6 10h12 M10 15h4 M11 20h2',
+ bookmark2: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z',
+ pin: 'M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
+ expand: 'M15 3h6v6 M9 21H3v-6 M21 3l-7 7 M3 21l7-7',
+ panelLeft: 'M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M9 4v16',
+ };
+ function Icon({ name, size, style, className, strokeWidth }) {
+ const d = P[name];
+ if (!d) return null;
+ const segs = d.split(' M').map((s, i) => (i === 0 ? s : 'M' + s));
+ return React.createElement('svg', {
+ width: size || 20, height: size || 20, viewBox: '0 0 24 24', fill: 'none',
+ stroke: 'currentColor', strokeWidth: strokeWidth || 1.8, strokeLinecap: 'round',
+ strokeLinejoin: 'round', style, className, 'aria-hidden': true,
+ }, segs.map((seg, i) => React.createElement('path', { key: i, d: seg })));
+ }
+
+export { Icon };
+export default Icon;
diff --git a/src/components/portal/map.jsx b/src/components/portal/map.jsx
new file mode 100644
index 0000000..ceabbfe
--- /dev/null
+++ b/src/components/portal/map.jsx
@@ -0,0 +1,171 @@
+'use client';
+/* Interactive stylized sector map — pan via plots, zoom, layers, hover, select. */
+import React, { useState, useMemo } from 'react';
+import Icon from './icons';
+
+ // Generate a plausible plot layout: blocks of plots separated by roads.
+ function buildPlots(seed) {
+ const rng = (() => { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; })();
+ const blocks = [
+ { code: 'A', x: 40, y: 40, cols: 6, rows: 3 },
+ { code: 'B', x: 40, y: 230, cols: 6, rows: 3 },
+ { code: 'C', x: 470, y: 40, cols: 5, rows: 3 },
+ { code: 'D', x: 470, y: 230, cols: 5, rows: 4 },
+ ];
+ const pw = 58, ph = 50, gap = 6;
+ const plots = [];
+ blocks.forEach(b => {
+ for (let r = 0; r < b.rows; r++) for (let c = 0; c < b.cols; c++) {
+ const n = r * b.cols + c + 1;
+ const types = ['res', 'res', 'res', 'comm', 'inst', 'park'];
+ const t = rng() < 0.08 ? 'park' : rng() < 0.12 ? 'comm' : rng() < 0.08 ? 'inst' : 'res';
+ plots.push({
+ id: b.code + '-' + n, block: b.code,
+ x: b.x + c * (pw + gap), y: b.y + r * (ph + gap), w: pw, h: ph, type: t,
+ });
+ }
+ });
+ return plots;
+ }
+
+ const TYPE = {
+ res: { fill: '#ffffff', label: 'Residential', sw: '#cbd8e8' },
+ comm: { fill: '#fff2dc', label: 'Commercial', sw: '#f0d49a' },
+ inst: { fill: '#e7eeff', label: 'Institutional', sw: '#bcd0f5' },
+ park: { fill: '#e3f6ec', label: 'Green / Park', sw: '#b6e6cd' },
+ };
+
+ function SectorMap({ sectorName = 'Sector 18', myPlot = 'B-47', height = 460, compact }) {
+ const plots = useMemo(() => buildPlots(sectorName.length * 137 + 7), [sectorName]);
+ const [zoom, setZoom] = useState(1);
+ const [sel, setSel] = useState(null);
+ const [hover, setHover] = useState(null);
+ const [layers, setLayers] = useState({ satellite: false, master: false, infra: true });
+ const [search, setSearch] = useState('');
+ // map myPlot to an existing plot id; fall back to one in the same block, then a default
+ const mine = plots.find(p => p.id === myPlot)
+ || (myPlot && plots.find(p => p.block === myPlot.split('-')[0]))
+ || plots[14];
+ const matched = search ? plots.find(p => p.id.toLowerCase() === search.toLowerCase()) : null;
+
+ const W = 760, H = 480;
+ // Real satellite raster (Esri World Imagery export — no API key) used as the map base; plots draw on top
+ const satImg = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/export?bbox=77.5285,28.148,77.5715,28.172&bboxSR=4326&imageSR=4326&size=760,480&format=jpg&transparent=false&f=image';
+ return (
+
+ {/* toolbar */}
+
+
+
{sectorName}
+
+
+ { setSearch(e.target.value); const m = plots.find(p => p.id.toLowerCase() === e.target.value.toLowerCase()); if (m) setSel(m.id); }} />
+
+
+
+ {[['satellite', 'layers', 'Satellite'], ['master', 'grid', 'Master Plan'], ['infra', 'building', 'Infrastructure']].map(([k, ic, l]) => (
+ setLayers(s => ({ ...s, [k]: !s[k] }))}
+ className="chip" style={{ padding: '7px 11px', fontSize: 12.5, background: layers[k] ? 'var(--navy-800)' : '#fff', color: layers[k] ? '#fff' : 'var(--ink-2)', borderColor: layers[k] ? 'var(--navy-800)' : 'var(--line)' }}>
+ {l}
+
+ ))}
+
+
+
+ {/* map canvas */}
+
+
+
+ {/* base — real satellite imagery when Satellite layer is on, else flat land. Plots stay drawn on top. */}
+ {layers.satellite
+ ?
+ : }
+ {/* roads */}
+
+
+
+
+
+
+ {/* master plan grid overlay */}
+ {layers.master && Array.from({ length: 20 }).map((_, i) => (
+
+
+
+
+ ))}
+ {/* plots */}
+ {plots.map(p => {
+ const t = TYPE[p.type];
+ const isMine = p.id === mine.id;
+ const isSel = sel === p.id;
+ const isHover = hover === p.id;
+ const isMatch = matched && matched.id === p.id;
+ let fill = t.fill;
+ if (isMine) fill = 'var(--emerald-500)';
+ if (isSel || isMatch) fill = 'var(--blue-500)';
+ const plain = !isMine && !isSel && !isMatch;
+ return (
+ setHover(p.id)} onMouseLeave={() => setHover(null)}
+ onClick={() => setSel(p.id)}>
+
+ {p.id}
+
+ );
+ })}
+ {/* my plot pulse */}
+
+
+
+
+ {/* satellite ribbon */}
+ {layers.satellite && (
+
+ Satellite · {sectorName}
+
+ )}
+
+ {/* legend */}
+ {!compact && (
+
+
Legend
+ {[['Your Plot', 'var(--emerald-500)'], ['Selected', 'var(--blue-500)'], ['Residential', '#fff'], ['Commercial', '#fff2dc'], ['Green / Park', '#e3f6ec']].map(([l, c]) => (
+
+ {l}
+
+ ))}
+
+ )}
+
+ {/* zoom controls */}
+
+ setZoom(z => Math.min(2.2, z + 0.25))}>
+ setZoom(z => Math.max(0.7, z - 0.25))}>
+ { setZoom(1); setSel(null); }}>
+
+
+ {/* selected plot tooltip */}
+ {sel && (() => { const p = plots.find(x => x.id === sel); const isMine = p.id === mine.id; return (
+
+
Plot {p.id} setSel(null)} style={{ border: 'none', background: 'none', color: 'var(--faint)', cursor: 'pointer' }}>
+
{TYPE[p.type].label} · Block {p.block}
+
+
+
Area {p.type === 'comm' ? '450' : '300'} sq.m
+
Status {isMine ? Yours : Allotted }
+
+
+ ); })()}
+
+
+ );
+ }
+
+export { SectorMap };
+export default SectorMap;
diff --git a/src/components/portal/myjourney.jsx b/src/components/portal/myjourney.jsx
new file mode 100644
index 0000000..70cfaae
--- /dev/null
+++ b/src/components/portal/myjourney.jsx
@@ -0,0 +1,370 @@
+'use client';
+/* Construction Journey — goal selection + 8-step survey + Project Team + Progress Center */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { PageHead, Donut, Avatar } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA, fmt = D.fmtINR;
+
+ function MyJourney({ go, toast }) {
+ const [tab, setTab] = useState('journey');
+ return (
+
+
+ go('sampleplans')}> Design Explorer
+ go('construction')}> Construction Module
+
+
+ {/* sub-tabs */}
+
+ {[['journey', 'Journey Steps'], ['team', 'Project Team'], ['progress', 'Progress Center']].map(([k, l]) => (
+ setTab(k)} style={{ padding: '11px 4px', marginRight: 22, background: 'none', border: 'none', borderBottom: '2.5px solid ' + (tab === k ? 'var(--blue-600)' : 'transparent'), color: tab === k ? 'var(--ink)' : 'var(--muted)', fontWeight: tab === k ? 700 : 500, fontSize: 14.5, cursor: 'pointer', marginBottom: -1 }}>{l}
+ ))}
+
+
+ {tab === 'journey' &&
}
+ {tab === 'team' &&
}
+ {tab === 'progress' &&
}
+
+ );
+ }
+
+ /* ---------------- GOAL SELECTION + 8-STEP ACCORDION SURVEY ---------------- */
+ function JourneySteps({ go, toast }) {
+ const [goal, setGoal] = useState('start');
+ const [steps, setSteps] = useState(D.journeySteps);
+ const [open, setOpen] = useState(() => {
+ const a = D.journeySteps.findIndex(s => s.active);
+ return a >= 0 ? a : 0;
+ });
+
+ const doneCount = steps.filter(s => s.done).length;
+ const save = (i) => {
+ setSteps(arr => arr.map((s, j) => j === i ? { ...s, done: true, active: false } : s));
+ toast && toast('Information saved · ' + steps[i].title);
+ const next = steps.findIndex((s, j) => j > i && !s.done);
+ setOpen(next >= 0 ? next : -1);
+ };
+
+ return (
+
+ {/* progress header */}
+
+
+
+
You're {Math.round(doneCount / steps.length * 100)}% through your journey
+
Complete each step below — saved steps turn green and stay editable. We'll guide you to the next action.
+
+ {doneCount} completed
+ {steps.length - doneCount} remaining
+
+
+
+
+ {/* goal selection */}
+
+
What would you like to do with your plot?
+
Pick a goal — we'll personalise your journey around it.
+
+ {D.journeyGoals.map(gOpt => {
+ const on = goal === gOpt.id;
+ return (
+
setGoal(gOpt.id)} className="card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: '14px 15px', borderRadius: 13, border: '1.5px solid ' + (on ? 'var(--blue-400)' : 'var(--line)'), background: on ? 'var(--blue-50)' : '#fff', display: 'flex', gap: 12, alignItems: 'flex-start', boxShadow: on ? '0 0 0 4px rgba(38,112,230,.1)' : 'none' }}>
+
+
+
{gOpt.title}
+
{gOpt.desc}
+
+
+ );
+ })}
+
+
+
+ {/* accordion survey */}
+
+ {steps.map((s, i) => {
+ const isOpen = open === i;
+ const state = s.done ? 'done' : s.active || isOpen ? 'active' : 'todo';
+ return (
+
+ {/* header row */}
+
setOpen(isOpen ? -1 : i)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 14, padding: '15px 18px', background: s.done ? 'var(--emerald-50)' : '#fff', border: 'none', cursor: 'pointer', textAlign: 'left' }}>
+
+ {s.done ? : i + 1}
+
+
+
+ {s.title}
+ {s.done && Information Saved }
+ {state === 'active' && !s.done && In progress }
+
+
{s.done ? s.summary : s.desc}
+
+
+
+
+ {/* body */}
+ {isOpen && (
+
+
+
+ save(i)}> {s.done ? 'Update & Save' : 'Save & Continue'}
+ {s.done && Editable anytime }
+
+
+ )}
+
+ );
+ })}
+
+
+ );
+ }
+
+ function StepBody({ step, go }) {
+ // tailored, lightweight content per step
+ switch (step.key) {
+ case 'goal':
+ return
Your selected goal drives the recommended steps, designs and budget. You can change it from the selector above. ;
+ case 'plot':
+ return (
+
+ {[['Allotment No.', D.user.allotment], ['Plot', D.user.plot], ['Sector', D.user.sector], ['Pocket', D.user.pocket]].map(([l, v]) => )}
+
+ );
+ case 'design':
+ return (
+
+ Selected design from the Design Explorer. Browse alternatives anytime.
+ go('sampleplans')}> Open Design Explorer
+
+ );
+ case 'planning':
+ return (
+
+ {[['Configuration', 'G+1'], ['Built-up area', '4,200 sq.ft'], ['Est. duration', '11 months'], ['Plan status', 'Sanctioned']].map(([l, v]) => )}
+
+ );
+ case 'budget':
+ return (
+
+
+ {[['Total budget', fmt(D.construction.estimatedCost)], ['Milestones', '5'], ['Funded', fmt(D.construction.spent)]].map(([l, v]) => )}
+
+
go('payments')}> Set up milestone payments
+
+ );
+ case 'contractor':
+ return (
+
+ Assign your project team — manager, site engineer, architect & crew. View the Project Team tab to manage contacts.
+
+ );
+ case 'progress':
+ return
Once construction starts, track every stage with photos, notes and dates in the Progress Center. ;
+ case 'certificate':
+ return
After final inspection, apply for and download your Completion Certificate here. ;
+ default:
+ return null;
+ }
+ }
+
+ function Field({ label, value }) {
+ return (
+
+ );
+ }
+ function Note({ children }) {
+ return
{children}
;
+ }
+
+ /* ---------------- PROJECT TEAM ---------------- */
+ function ProjectTeam({ toast }) {
+ return (
+
+
+
Your Project Team
+
+ {D.projectTeam.map(m => (
+
+
+
{m.name}
+
{m.org}
+
+ toast && toast('Calling ' + m.name)}> Call
+ toast && toast('Message sent')}> Message
+
+
+ ))}
+
+
+ {/* team updates / milestone */}
+
+
Team Updates
+
+ {D.teamUpdates.map((u, i, arr) => (
+
+ {i !== arr.length - 1 &&
}
+
+
+
{u.who} — {u.text}
+
{u.time}
+
+
+ ))}
+
+
+
+ );
+ }
+
+ /* ---------------- PROGRESS CENTER (stages + live view) ---------------- */
+ function ProgressCenter() {
+ const overall = Math.round(D.progressStages.reduce((s, x) => s + x.pct, 0) / D.progressStages.length);
+ return (
+
+ {/* live view + summary */}
+
+
+
+
+
+ LIVE
+
+
+
+
Live Site Camera
+
Plot B-47 · Cam 1 · 1080p — drop in your RTSP/webcam feed
+
+
+
+
{['Cam 1', 'Cam 2', 'Drone'].map((c, i) => {c} )}
+
Updated just now
+
+
+
+
+
+ {[['Daily Site Photos', 'image', '42 this month'], ['Weekly Progress Report', 'documents', 'Latest: 06 Jun'], ['Current Stage', 'construction', 'Slab Casting']].map(([l, ic, v]) => (
+
+ {l}
+ {v}
+
+ ))}
+
+
+
+
+ {/* stage timeline */}
+
+
Construction Stages
+
+ {D.progressStages.map((st, i, arr) => {
+ const state = st.pct === 100 ? 'done' : st.pct > 0 ? 'active' : 'todo';
+ const cc = state === 'done' ? 'var(--emerald-500)' : state === 'active' ? 'var(--blue-500)' : '#c2cedd';
+ return (
+
+ {i !== arr.length - 1 &&
}
+
+ {state === 'done' ? : state === 'active' ? : {i + 1} }
+
+
+
+
{st.name}
+
+ {st.photos > 0 && {st.photos} }
+ {state === 'done' ? 'Completed' : state === 'active' ? st.pct + '%' : 'Upcoming'}
+
+
+ {st.note &&
{st.note}
}
+
{st.date}
+ {state === 'active' &&
}
+
+
+ );
+ })}
+
+
+
+
+
+ );
+ }
+
+ /* ---------------- GALLERY CENTER (Drive-style folders) ---------------- */
+ function GalleryCenter() {
+ const [view, setView] = useState('grid'); // grid (large icons) | list
+ const [open, setOpen] = useState(null); // open folder
+
+ if (open) {
+ return (
+
+
+
setOpen(null)}> All folders
+
{open.name} {open.count} photos
+
+
+ {[...Array(open.count)].map((_, i) => (
+
+
+ IMG_{String(i + 1).padStart(3, '0')}
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+
+
Gallery Center
+
Daily site photos organised in folders — click to open.
+
+
+ {[['grid', 'grid'], ['list', 'menu']].map(([k, ic]) => (
+ setView(k)} className="icon-btn" style={{ width: 34, height: 34, background: view === k ? '#fff' : 'transparent', boxShadow: view === k ? 'var(--sh-xs)' : 'none', color: view === k ? 'var(--ink)' : 'var(--muted)' }}>
+ ))}
+
+
+
+ {view === 'grid' ? (
+
+ {D.galleryFolders.map(fo => (
+
setOpen(fo)} className="card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: 14, borderRadius: 13, border: '1px solid var(--line)', background: '#fff' }}>
+
+
+
+ {fo.name}
+ {fo.count} photos · {fo.date}
+
+ ))}
+
+ ) : (
+
+ {D.galleryFolders.map(fo => (
+ setOpen(fo)} className="row spread card-hover" style={{ width: '100%', textAlign: 'left', cursor: 'pointer', padding: '12px 14px', borderRadius: 11, border: '1px solid var(--line)', background: '#fff' }}>
+ {fo.name}
+ {fo.count} photos · {fo.date}
+
+ ))}
+
+ )}
+
+ );
+ }
+
+export { MyJourney };
+export default MyJourney;
diff --git a/src/components/portal/nav.js b/src/components/portal/nav.js
new file mode 100644
index 0000000..f08362c
--- /dev/null
+++ b/src/components/portal/nav.js
@@ -0,0 +1,53 @@
+'use client';
+/* Maps the legacy route keys used throughout the portal components to real
+ Next.js URLs, and exposes a `go(routeKey)` navigator backed by next/router. */
+import { useRouter } from 'next/navigation';
+
+export const ROUTE_PATHS = {
+ // public / auth
+ welcome: '/portal/login',
+ login: '/portal/login',
+ 'login-allotment': '/portal/login/allotment',
+ verify: '/portal/verify',
+ otp: '/portal/verify',
+ register: '/portal/register',
+ 'sectormaps-public': '/portal/maps',
+ app: '/portal/dashboard',
+ // authenticated app
+ dashboard: '/portal/dashboard',
+ plot: '/portal/plot',
+ construction: '/portal/construction',
+ payments: '/portal/payments',
+ documents: '/portal/documents',
+ notices: '/portal/notices',
+ sectormaps: '/portal/sectormaps',
+ transfer: '/portal/transfer',
+ assistant: '/portal/assistant',
+ support: '/portal/support',
+ profile: '/portal/profile',
+ sampleplans: '/portal/sampleplans',
+ journey: '/portal/journey',
+ myjourney: '/portal/myjourney',
+};
+
+export function pathFor(routeKey) {
+ return ROUTE_PATHS[routeKey] || '/portal/' + routeKey;
+}
+
+/* Given a pathname like "/portal/payments", return the route key ("payments").
+ Used by the shell to highlight the active nav item. */
+export function routeKeyFromPath(pathname) {
+ if (!pathname) return 'dashboard';
+ const rest = pathname.replace(/^\/portal\/?/, '');
+ const seg = rest.split('/')[0];
+ return seg || 'dashboard';
+}
+
+/* Drop-in replacement for the old `go(route)` callback. */
+export function usePortalNav() {
+ const router = useRouter();
+ return (routeKey) => {
+ router.push(pathFor(routeKey));
+ if (typeof window !== 'undefined') window.scrollTo(0, 0);
+ };
+}
diff --git a/src/components/portal/onboarding.jsx b/src/components/portal/onboarding.jsx
new file mode 100644
index 0000000..933e6a0
--- /dev/null
+++ b/src/components/portal/onboarding.jsx
@@ -0,0 +1,574 @@
+'use client';
+/* Registration wizard: Account → Allotment → Verify → Addresses */
+import React, { useState, useEffect, useRef } from 'react';
+import Icon from './icons';
+import YEIDA from './data';
+import { AuthShell, VerifyPanel } from './auth';
+import { Avatar } from './ui';
+import { useUserPhoto, readImageFile, setUserProfile } from './user-store';
+
+const D = YEIDA;
+
+ const STEPS = [
+ { k: 'account', label: 'Account', icon: 'user' },
+ { k: 'allotment', label: 'Allotment', icon: 'building' },
+ { k: 'verify', label: 'Verify', icon: 'shield' },
+ { k: 'address', label: 'Address', icon: 'mapPin' },
+ ];
+
+ function Stepper({ step }) {
+ return (
+
+ {STEPS.map((s, i) => (
+
+
+
+ {i < step ? : i + 1}
+
+ {s.label}
+
+ {i < STEPS.length - 1 && }
+
+ ))}
+
+ );
+ }
+
+ const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ const ccOf = (code) => D.countryCodes.find(c => c.code === code) || D.countryCodes[0];
+
+ function Register({ go }) {
+ const [step, setStep] = useState(0);
+ const [legal, setLegal] = useState(null); // 'terms' | 'privacy' | null
+ const [form, setForm] = useState({
+ firstName: 'Rajeev', lastName: 'Malhotra', email: 'r.malhotra@email.com',
+ cc: '+91', mobile: '9876500012', password: '', confirm: '',
+ relationship: 'Self', inviteCode: '', inviteValid: false,
+ terms: false, reviewedTerms: false, reviewedPrivacy: false, cookieChoice: null,
+ allotteeFirst: 'Rajeev', allotteeLast: 'Malhotra',
+ hasAllotment: true, allotment: 'YEA-654321', sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47', plotVerified: false,
+ reg: { country: 'IN', line1: 'Plot B-47, Pocket B, Sector 18', line2: 'Yamuna Expressway, Gautam Buddh Nagar', city: 'Greater Noida', state: 'Uttar Pradesh', postal: '203201' },
+ mail: { country: 'IN', line1: 'C-204, Orchid Residency, Sector 50', line2: 'Near City Centre', city: 'Noida', state: 'Uttar Pradesh', postal: '201301' },
+ });
+ const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
+ const setAddr = (prefix, patch) => setForm(f => ({ ...f, [prefix]: { ...f[prefix], ...patch } }));
+ const next = () => setStep(s => Math.min(STEPS.length - 1, s + 1));
+ const isAllottee = form.relationship === 'Self';
+
+ // --- Step 0 validity gate (errors surface before the flow can continue) ---
+ const cc = ccOf(form.cc);
+ const phoneOk = form.mobile.replace(/\D/g, '').length === cc.digits;
+ const emailOk = emailRe.test(form.email);
+ const pwOk = form.password.length >= 8 && form.password === form.confirm;
+ const inviteOk = isAllottee || (form.inviteValid && !!form.allotteeFirst && !!form.allotteeLast);
+ const step0Valid = !!form.firstName && !!form.lastName && emailOk && phoneOk && pwOk && form.terms && inviteOk;
+ // Step 1: either the allotment number OR a verified plot number is enough.
+ const step1Valid = form.hasAllotment ? !!form.allotment.trim() : !!form.plotVerified;
+ const canContinue = step === 0 ? step0Valid : step === 1 ? step1Valid : true;
+
+ const markReviewed = (key) => set(key === 'terms' ? 'reviewedTerms' : 'reviewedPrivacy', true);
+
+ // Persist the registrant's identity so the whole portal reflects self vs on-behalf
+ const finish = () => {
+ const initials = ((form.firstName[0] || '') + (form.lastName[0] || '')).toUpperCase();
+ setUserProfile({
+ name: `${form.firstName} ${form.lastName}`.trim(),
+ firstName: form.firstName, lastName: form.lastName, initials,
+ email: form.email,
+ relationship: form.relationship,
+ isAllottee,
+ allotteeFirstName: isAllottee ? form.firstName : form.allotteeFirst,
+ allotteeLastName: isAllottee ? form.lastName : form.allotteeLast,
+ });
+ go('app');
+ };
+
+ return (
+ <>
+
+
+
step === 0 ? go('login') : setStep(s => s - 1)}> Back
+
New Allottee Registration
+
+ {['Create your account', 'Identify your plot', 'Verify email & phone', 'Your addresses'][step]}
+
+
+
+
+ {step === 0 &&
}
+ {step === 1 &&
}
+ {step === 2 &&
}
+ {step === 3 &&
}
+
+
+ {step !== 2 && (
+ <>
+
step === STEPS.length - 1 ? finish() : next()}>
+ {step === 0 ? 'Create Account & Verify' : step === 3 ? 'Finish & Enter Portal' : 'Continue'}
+
+
+ {step === 0 && !step0Valid && (
+
+ Complete all fields, verify the mobile format and accept the Terms to continue.
+
+ )}
+ >
+ )}
+
+ {step === 0 && (
+
+ Already registered?{' '}
+ go('login')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', fontSize: 14 }}>Sign in
+
+ )}
+
+
+
+ {legal &&
setLegal(null)} onReviewed={markReviewed} />}
+ set('cookieChoice', c)} openPolicy={() => setLegal('privacy')} />
+ >
+ );
+ }
+
+ /* Step 0 — Account creation */
+ function AccountStep({ form, set, isAllottee, openLegal }) {
+ const [photo, setPhoto] = useUserPhoto();
+ const photoRef = useRef(null);
+ const onPhoto = (e) => { const f = e.target.files && e.target.files[0]; if (f) readImageFile(f, setPhoto); };
+ const cc = ccOf(form.cc);
+ const digits = form.mobile.replace(/\D/g, '');
+ const phoneOk = digits.length === cc.digits;
+ const phoneTouched = digits.length > 0;
+ const emailOk = emailRe.test(form.email);
+ const emailTouched = form.email.length > 0;
+ const pwShort = form.password.length > 0 && form.password.length < 8;
+ const pwMismatch = form.confirm.length > 0 && form.password !== form.confirm;
+ const reviewedBoth = form.reviewedTerms && form.reviewedPrivacy;
+
+ const onPhone = (e) => set('mobile', e.target.value.replace(/\D/g, '').slice(0, cc.digits));
+ const onCc = (e) => {
+ const ncc = ccOf(e.target.value);
+ set('cc', ncc.code);
+ set('mobile', form.mobile.replace(/\D/g, '').slice(0, ncc.digits));
+ };
+
+ // Invitation code — alphanumeric, validated on entry (format intentionally not revealed)
+ const [inviteState, setInviteState] = useState('idle'); // idle|checking|valid|invalid
+ const inviteTimer = useRef(null);
+ const onInvite = (e) => {
+ const code = e.target.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
+ set('inviteCode', code);
+ if (inviteTimer.current) clearTimeout(inviteTimer.current);
+ if (!code) { setInviteState('idle'); set('inviteValid', false); return; }
+ setInviteState('checking'); set('inviteValid', false);
+ inviteTimer.current = setTimeout(() => {
+ const ok = code.length >= 8 && /[A-Z]/.test(code) && /[0-9]/.test(code);
+ setInviteState(ok ? 'valid' : 'invalid');
+ set('inviteValid', ok);
+ }, 700);
+ };
+
+ return (
+
+ {/* Profile photo (optional) — carries through to the portal */}
+
+
+
+
photoRef.current && photoRef.current.click()} title="Upload photo" style={{ position: 'absolute', bottom: -2, right: -2, width: 26, height: 26, borderRadius: 99, border: '2px solid #fff', background: 'var(--blue-600)', color: '#fff', display: 'grid', placeItems: 'center', cursor: 'pointer', boxShadow: 'var(--sh-xs)' }}>
+
+
+
+
Profile photo
+
Optional — JPG or PNG. Shown across your portal.
+ {photo &&
setPhoto(null)} style={{ background: 'none', border: 'none', color: 'var(--red-600)', fontSize: 11.5, fontWeight: 600, cursor: 'pointer', padding: 0, marginTop: 2 }}>Remove photo }
+
+
+
+
Email Address
+
set('email', e.target.value)} placeholder="you@email.com" style={emailTouched && !emailOk ? { borderColor: 'var(--red-500)' } : undefined} />
+ {emailTouched && !emailOk &&
Enter a valid email address}
+
+
Mobile Number
+
+
{D.countryCodes.map(c => {c.label} {c.code} )}
+
+ {phoneOk && }
+
+
+ {phoneTouched && !phoneOk
+ ?
Enter a valid {cc.name} mobile number ({cc.digits} digits)
+ :
Validated against {cc.name} numbering rules }
+
+
+ {pwShort &&
Password must be at least 8 characters
}
+ {pwMismatch &&
Passwords do not match
}
+
+ {/* Relationship with Allottee */}
+
+ Relationship with Allottee
+ set('relationship', e.target.value)}>
+ {D.relationshipOptions.map(o => {o} )}
+
+
+
+
+ {isAllottee ? 'Self registration — you are the original allottee (Allottee = User).' : `Invitation registration — you are registering on behalf of the allottee as ${form.relationship} (Allottee ≠ User).`}
+
+
+ {/* Invitation code + allottee name — required when registrant is NOT the allottee */}
+ {!isAllottee && (
+
+
Original Allottee Details
+
+
+
Invitation Code
+
+
+
+ {inviteState === 'checking' && }
+ {inviteState === 'valid' && }
+
+ {inviteState === 'checking' &&
Verifying invitation code… }
+ {inviteState === 'valid' &&
Invitation code verified}
+ {inviteState === 'invalid' &&
We couldn't verify this invitation code}
+ {inviteState === 'idle' &&
Use the code issued to you by the Authority }
+
+
+
+
+ )}
+
+ {/* Terms — links open a popup; acceptance enabled only after both are reviewed */}
+
+ set('terms', e.target.checked)} style={{ width: 17, height: 17, accentColor: 'var(--blue-600)' }} />
+ I have read and accept YEIDA's{' '}
+ openLegal('terms')} style={{ background: 'none', border: 'none', padding: 0, color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', textDecoration: 'underline', fontSize: 13.5 }}>Terms & Conditions
+ {form.reviewedTerms && }
+ {' '}and{' '}
+ openLegal('privacy')} style={{ background: 'none', border: 'none', padding: 0, color: 'var(--blue-600)', fontWeight: 700, cursor: 'pointer', textDecoration: 'underline', fontSize: 13.5 }}>Privacy Policy
+ {form.reviewedPrivacy && }
+
+
+ {!reviewedBoth &&
Please open and review both documents to enable acceptance.
}
+
+ );
+ }
+
+ /* Step 1 — Allotment identification (allotment number OR plot number) */
+ function AllotmentStep({ form, set }) {
+ const [checking, setChecking] = useState(false);
+ const setPlot = (k, v) => { set(k, v); set('plotVerified', false); };
+ const verifyPlot = () => {
+ if (!form.plot.trim()) return;
+ setChecking(true);
+ setTimeout(() => { setChecking(false); set('plotVerified', true); }, 800); // demo lookup → allotment match
+ };
+ return (
+
+
How would you like to identify your plot?
+
Use your allotment number, or your plot number if you don't have it handy — either one is enough.
+
+ {/* toggle */}
+
+ {[['have', 'I have my allotment number'], ['forgot', 'Use my plot number']].map(([k, l]) => (
+ set('hasAllotment', k === 'have')}
+ style={{ padding: '8px 15px', borderRadius: 9, border: 'none', fontSize: 13, fontWeight: 600, cursor: 'pointer', background: (form.hasAllotment === (k === 'have')) ? '#fff' : 'transparent', color: (form.hasAllotment === (k === 'have')) ? 'var(--ink)' : 'var(--muted)', boxShadow: (form.hasAllotment === (k === 'have')) ? 'var(--sh-xs)' : 'none' }}>{l}
+ ))}
+
+
+ {form.hasAllotment ? (
+
+
Allotment Number
+
set('allotment', e.target.value.toUpperCase())} placeholder="e.g. YEA-654321" />
+
Format: YEA- followed by your 6-digit number
+
+ ) : (
+
+
+ Identify your plot below and verify it — we'll match it to your allotment record. No allotment number needed.
+
+
+
Sector
+ setPlot('sector', e.target.value)}>{D.sectors.map(s => {s.name} )}
+
+
Pocket
+ setPlot('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => {p} )}
+
+
Plot Number
+
setPlot('plot', e.target.value)} placeholder="3–4 digits" maxLength={5} />
+ {form.plotVerified && }
+
+
+
+
+ {form.plotVerified
+ ? Plot verified · matched to allotment YEA-654321
+ : Unverified — verify your plot to continue. }
+ {!form.plotVerified &&
+ {checking ? : }Verify plot
+ }
+
+
+ )}
+
+ );
+ }
+
+ /* Step 2 — Verification */
+ function VerifyStep({ form, go }) {
+ const [both, setBoth] = useState(false);
+ return (
+
+
Verify both your email and mobile number on this screen. Each requires an OTP.
+
+
{both ? 'Continue' : 'Verify email & phone to continue'}
+
+ );
+ }
+
+ /* Step 3 — Addresses (registered + mailing, editable independently) */
+ function AddressStep({ form, setAddr }) {
+ const [sameAsReg, setSameAsReg] = useState(false);
+ return (
+
+ );
+ }
+
+ // Country → State → dependent fields. India auto-fills from PIN; US/UK use a postcode lookup; others manual.
+ function AddressBlock({ title, sub, icon, value, onChange }) {
+ const country = D.addressCountries.find(c => c.code === value.country) || D.addressCountries[0];
+ const [pinState, setPinState] = useState('idle'); // idle|checking|filled|notfound (India)
+ const [candidates, setCandidates] = useState(null); // postcode lookup results (US/UK)
+ const [lookupState, setLookupState] = useState('idle'); // idle|checking|done|notfound
+
+ const onCountry = (e) => {
+ const c = D.addressCountries.find(x => x.code === e.target.value) || D.addressCountries[0];
+ setPinState('idle'); setCandidates(null); setLookupState('idle');
+ onChange({ country: c.code, state: c.states[0] || '', city: '', line1: '', line2: '', postal: '' });
+ };
+
+ // India: auto-fill on a complete 6-digit PIN
+ const onPin = (e) => {
+ const pin = e.target.value.replace(/\D/g, '').slice(0, country.postalLen);
+ onChange({ postal: pin });
+ if (country.lookup !== 'pincode') return;
+ if (pin.length < country.postalLen) { setPinState('idle'); return; }
+ setPinState('checking');
+ setTimeout(() => {
+ const hit = D.pincodeLookup[pin];
+ if (hit) { onChange({ postal: pin, city: hit.city, state: hit.state }); setPinState('filled'); }
+ else setPinState('notfound');
+ }, 600);
+ };
+
+ // US/UK: postcode lookup service returns address candidates
+ const runLookup = () => {
+ const key = value.postal.replace(/\s/g, '').toUpperCase();
+ setLookupState('checking'); setCandidates(null);
+ setTimeout(() => {
+ const hits = D.postcodeLookup[key];
+ if (hits && hits.length) { setCandidates(hits); setLookupState('done'); }
+ else setLookupState('notfound');
+ }, 700);
+ };
+ const pickCandidate = (c) => { onChange({ line1: c.line1, city: c.city, state: c.state }); setCandidates(null); setLookupState('done'); };
+
+ const stateField = country.states.length ? (
+ State / Region
+ onChange({ state: e.target.value })}>
+ Select…
+ {country.states.map(s => {s} )}
+
+
+ ) : (
+ State / Region onChange({ state: e.target.value })} placeholder="State / region" />
+ );
+
+ return (
+
+
+
+
+ {/* 1. Country first */}
+
Country
+
+
+ {D.addressCountries.map(c => {c.name} )}
+
+
+
+
+ {/* 2. India — PIN drives auto-fill */}
+ {country.lookup === 'pincode' && (
+
{country.postalLabel}
+
+ {pinState === 'checking' && }
+
+ {pinState === 'filled' &&
Auto-filled city & state from PIN}
+ {pinState === 'notfound' &&
PIN not found — please fill the fields manually}
+
+ )}
+
+ {/* 2. US/UK — postcode lookup service */}
+ {country.lookup === 'postcode' && (
+
{country.postalLabel}
+
+
{ onChange({ postal: e.target.value.toUpperCase() }); setLookupState('idle'); }} placeholder={`Enter ${country.postalLabel}`} />
+
+ {lookupState === 'checking' ? : }
+ Find address
+
+
+ {lookupState === 'notfound' &&
No match — please fill the fields manually}
+ {candidates && (
+
+ {candidates.map((c, i) => (
+ pickCandidate(c)} style={{ display: 'block', width: '100%', textAlign: 'left', padding: '9px 12px', border: 'none', borderTop: i ? '1px solid var(--line)' : 'none', background: '#fff', cursor: 'pointer', fontSize: 13 }}>
+ {c.line1} · {c.city}, {c.state}
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* 3. State (dependent dropdown) */}
+ {stateField}
+
+ {/* 4. Remaining address fields */}
+
Address Line 1 onChange({ line1: e.target.value })} placeholder="House / flat, street" />
+
Address Line 2 onChange({ line2: e.target.value })} placeholder="Area / landmark (optional)" />
+
+
+
+ );
+ }
+
+ /* Legal popup — Terms / Privacy. Acceptance is enabled once the reader scrolls to the end. */
+ function LegalModal({ docKey, onClose, onReviewed }) {
+ const doc = D.legalDocs[docKey];
+ const bodyRef = useRef(null);
+ const [atEnd, setAtEnd] = useState(false);
+
+ const reachEnd = () => { setAtEnd(true); if (onReviewed) onReviewed(docKey); };
+ useEffect(() => {
+ const el = bodyRef.current; if (!el) return;
+ if (el.scrollHeight <= el.clientHeight + 8) reachEnd();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ const onScroll = () => {
+ const el = bodyRef.current; if (!el) return;
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 24) reachEnd();
+ };
+
+ return (
+
+
e.stopPropagation()} className="anim-pop card" style={{ width: 640, maxWidth: '100%', maxHeight: '86vh', padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
+
+
{doc.title}
Last updated {doc.updated}
+
+
+
+ {doc.sections.map((s, i) => (
+
+ ))}
+
— End of document —
+
+
+
+ {atEnd ? 'Reviewed — you can now accept' : 'Scroll to the end to mark as reviewed'}
+
+ {atEnd ? 'Done' : 'Close'}
+
+
+
+ );
+ }
+
+ /* Cookie consent — separate GDPR-style banner (independent of the Terms checkbox) */
+ function CookieBanner({ choice, onChoice, openPolicy }) {
+ if (choice) return null;
+ return (
+
+
+
+
+
We value your privacy
+
+ We use strictly necessary cookies to keep the portal secure. With your consent we also use analytics & preference cookies. In line with GDPR you can accept or reject non-essential cookies — see our{' '}
+ Cookie & Privacy Policy .
+
+
+
+ onChoice('rejected')}>Reject non-essential
+ onChoice('accepted')}>Accept all
+
+
+
+ );
+ }
+
+ /* Standalone allotment lookup (login path — "I don't remember my allotment number") */
+ function AllotmentLookup({ go }) {
+ const [form, setForm] = useState({ sector: 'Sector 18', pocket: 'Pocket B', plot: 'B-47' });
+ const [found, setFound] = useState(false);
+ const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
+ return (
+
+
+
go('login')}> Back
+
Find your allotment
+
Identify your plot
+
No allotment number? Locate your plot by sector, pocket and plot number.
+
+
Sector set('sector', e.target.value)}>{D.sectors.map(s => {s.name} )}
+
Pocket set('pocket', e.target.value)}>{['Pocket A', 'Pocket B', 'Pocket C', 'Pocket D'].map(p => {p} )}
+
+
+ {found ? (
+
+
Allotment found · YEA-654321
{form.sector} · {form.pocket} · Plot {form.plot}
+
+ ) : (
+
setFound(true)}> Find my allotment
+ )}
+
go('verify')}>Continue to Verification
+
+
+ );
+ }
+
+export { Register, AllotmentLookup };
diff --git a/src/components/portal/pages.tsx b/src/components/portal/pages.tsx
new file mode 100644
index 0000000..54c8fba
--- /dev/null
+++ b/src/components/portal/pages.tsx
@@ -0,0 +1,51 @@
+/* Typed barrel for the portal's untyped (.jsx) components.
+ The page/layout components are authored in plain JSX, so TypeScript infers
+ narrow, required prop shapes from their destructuring. These pages are driven
+ by a shared { go, toast } context, so we re-export them as permissive
+ component types — one casting point instead of per-call-site casts. */
+import type { ComponentType } from 'react';
+
+import IconImpl from './icons';
+import { Logo as LogoImpl, Avatar as AvatarImpl, PageHead as PageHeadImpl } from './ui';
+import { Shell as ShellImpl } from './shell';
+import { Dashboard as DashboardImpl } from './dashboard';
+import { MyPlot as MyPlotImpl } from './plot';
+import { Construction as ConstructionImpl } from './construction';
+import { Payments as PaymentsImpl } from './payments';
+import { Documents as DocumentsImpl } from './documents';
+import {
+ Notices as NoticesImpl,
+ SectorMaps as SectorMapsImpl,
+ Support as SupportImpl,
+ Profile as ProfileImpl,
+} from './extras';
+import { Transfer as TransferImpl } from './transfer';
+import { Assistant as AssistantImpl } from './assistant';
+import { SamplePlans as SamplePlansImpl, Journey as JourneyImpl } from './sampleplans';
+import { MyJourney as MyJourneyImpl } from './myjourney';
+
+type AnyProps = Record;
+type C = ComponentType;
+
+// Shared UI primitives
+export const Icon = IconImpl as unknown as C;
+export const Logo = LogoImpl as unknown as C;
+export const Avatar = AvatarImpl as unknown as C;
+export const PageHead = PageHeadImpl as unknown as C;
+export const Shell = ShellImpl as unknown as C;
+
+// Authenticated pages
+export const Dashboard = DashboardImpl as unknown as C;
+export const MyPlot = MyPlotImpl as unknown as C;
+export const Construction = ConstructionImpl as unknown as C;
+export const Payments = PaymentsImpl as unknown as C;
+export const Documents = DocumentsImpl as unknown as C;
+export const Notices = NoticesImpl as unknown as C;
+export const SectorMaps = SectorMapsImpl as unknown as C;
+export const Support = SupportImpl as unknown as C;
+export const Profile = ProfileImpl as unknown as C;
+export const Transfer = TransferImpl as unknown as C;
+export const Assistant = AssistantImpl as unknown as C;
+export const SamplePlans = SamplePlansImpl as unknown as C;
+export const Journey = JourneyImpl as unknown as C;
+export const MyJourney = MyJourneyImpl as unknown as C;
diff --git a/src/components/portal/payments.jsx b/src/components/portal/payments.jsx
new file mode 100644
index 0000000..269441d
--- /dev/null
+++ b/src/components/portal/payments.jsx
@@ -0,0 +1,260 @@
+'use client';
+/* Payments module */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { PageHead, LineChart, BarChart } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA, fmt = D.fmtINR;
+
+ function Payments({ go, toast }) {
+ const [tab, setTab] = useState('dues');
+ const [pay, setPay] = useState(null); // due being paid
+ const totalDue = D.dues.reduce((s, d) => s + d.amount, 0);
+
+ return (
+
+
+ setTab('history')}> History
+ setPay({ label: 'All Outstanding Dues', amount: totalDue })}> Pay All Dues
+
+
+ {/* summary cards */}
+
+ {[
+ ['Outstanding', fmt(totalDue), 'amber', 'rupee', '3 dues pending'],
+ ['Paid (FY 25-26)', fmt(265370), 'emerald', 'checkCircle', '5 transactions'],
+ ['Next Due', '30 Jun', 'blue', 'calendar', 'Lease Rent 2026-27'],
+ ['Total Premium', fmt(D.property.totalValue), 'navy', 'building', '90% paid'],
+ ].map(([l, v, c, ic, sub]) => {
+ const cc = { blue: ['var(--blue-50)', 'var(--blue-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c];
+ return (
+
+ );
+ })}
+
+
+ {/* tabs */}
+
+ {[['dues', 'Outstanding Dues'], ['history', 'Payment History'], ['receipts', 'Receipts'], ['construction', 'Construction Payments'], ['analytics', 'Financial Analytics']].map(([k, l]) => (
+ setTab(k)} style={{ padding: '11px 4px', marginRight: 18, background: 'none', border: 'none', borderBottom: '2.5px solid ' + (tab === k ? 'var(--blue-600)' : 'transparent'), color: tab === k ? 'var(--ink)' : 'var(--muted)', fontWeight: tab === k ? 700 : 500, fontSize: 14.5, cursor: 'pointer', marginBottom: -1 }}>{l}
+ ))}
+
+
+ {tab === 'dues' &&
toast && toast('e-Challan generated · CH-2026-0606-18')} />}
+ {tab === 'history' && }
+ {tab === 'receipts' && }
+ {tab === 'construction' && }
+ {tab === 'analytics' && }
+
+ {pay && setPay(null)} onDone={() => { setPay(null); toast && toast('Payment successful · receipt sent to email'); }} />}
+
+ );
+ }
+
+ function Dues({ onPay, onChallan }) {
+ return (
+
+
+ Description Type Due Date Status Amount
+
+ {D.dues.map(d => (
+
+ {d.label}
{d.id}
+ {d.type}
+ {d.due}
+ {d.status === 'overdue' ? 'Overdue' : d.status === 'due' ? 'Due soon' : 'Upcoming'}
+ {fmt(d.amount)}
+
+
+ Challan
+ onPay(d)}>Pay
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ function History() {
+ return (
+
+
+ Transaction Date Mode Challan Status Amount
+
+ {D.payments.map(p => (
+
+ {p.label}
{p.id}
+ {p.date}
+ {p.mode}
+ {p.challan}
+ {p.status}
+ {fmt(p.amount)}
+
+
+ ))}
+
+
+
+ );
+ }
+
+ function Receipts({ toast }) {
+ return (
+
+ {D.payments.map(p => (
+
+
+
+ Paid
+
+
{p.label}
+
{p.challan} · {p.date}
+
{fmt(p.amount)}
+
+ toast && toast('Receipt downloaded')}> Download
+
+
+
+ ))}
+
+ );
+ }
+
+ function ConstructionPayments({ toast }) {
+ const CP = D.constructionPayments;
+ const C = D.construction;
+ const paid = CP.filter(p => p.status === 'Paid').reduce((s, p) => s + p.amount, 0);
+ const upcoming = CP.filter(p => p.status !== 'Paid').reduce((s, p) => s + p.amount, 0);
+ return (
+
+
+ {[['Estimated Build Cost', fmt(C.estimatedCost), 'navy', 'building'], ['Paid to Contractor', fmt(paid), 'emerald', 'checkCircle'], ['Upcoming Milestones', fmt(upcoming), 'amber', 'clock']].map(([l, v, c, ic]) => {
+ const cc = { emerald: ['var(--emerald-50)', 'var(--emerald-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], navy: ['#e9eefb', 'var(--navy-700)'] }[c];
+ return (
+
+ );
+ })}
+
+
+
+
Milestone Payments & Invoices
+ Contractor · {C.contractor.name}
+
+
+ Milestone Invoice Vendor Date Status Amount
+
+ {CP.map(p => (
+
+ {p.milestone}
+ {p.id}
+ {p.vendor}
+ {p.date}
+ {p.status === 'Paid' ? : null}{p.status}
+ {fmt(p.amount)}
+ {p.status === 'Paid' ? toast && toast('Receipt downloaded')}> : toast && toast('Milestone payment scheduled')}>Pay }
+
+ ))}
+
+
+
+
+ );
+ }
+
+ function Analytics() {
+ return (
+
+
+
+
Cumulative payments
+
Premium + lease rent paid since allotment
+
+
2019 2021 2023 2025 2026
+
+
+
By category
+
Lifetime breakdown
+ {[['Location Premium', 82, 'var(--blue-500)'], ['Lease Rent', 11, 'var(--emerald-500)'], ['Fees & Charges', 5, 'var(--amber-500)'], ['Utilities', 2, 'var(--navy-700)']].map(([l, p, c]) => (
+
+ ))}
+
+
+
+
Yearly payments
+
Total paid per financial year (₹ lakh)
+
+
{['19', '20', '21', '22', '23', '24', '25', '26'].map(y => FY{y} )}
+
+
+ );
+ }
+
+ function PayModal({ due, onClose, onDone }) {
+ const [method, setMethod] = useState('upi');
+ const [stage, setStage] = useState('select');
+ const methods = [['upi', 'UPI', 'phone'], ['card', 'Debit/Credit Card', 'payments'], ['net', 'Net Banking', 'building']];
+ const pay = () => { setStage('processing'); setTimeout(() => setStage('done'), 1600); };
+ return (
+
+
e.stopPropagation()} className="anim-pop card" style={{ width: 440, maxWidth: '100%', padding: 0, overflow: 'hidden' }}>
+ {stage !== 'done' ? (
+ <>
+
+
Complete Payment
+
+
+
+
+
{due.label}
+
{fmt(due.amount)}
+
+
Payment Method
+
+ {methods.map(([k, l, ic]) => (
+ setMethod(k)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '13px 15px', borderRadius: 12, border: '1.5px solid ' + (method === k ? 'var(--blue-400)' : 'var(--line)'), background: method === k ? 'var(--blue-50)' : '#fff', cursor: 'pointer' }}>
+
+ {l}
+ {method === k && }
+
+ ))}
+
+
+ {stage === 'processing' ? <> Processing…> : <> Pay {fmt(due.amount)}>}
+
+
Secured by YEIDA Payment Gateway
+
+ >
+ ) : (
+
+
+
Payment Successful
+
{fmt(due.amount)} paid · Receipt CH-2026-0606-18
+
+ Receipt
+ Done
+
+
+ )}
+
+
+ );
+ }
+
+export { Payments };
+export default Payments;
diff --git a/src/components/portal/plot.jsx b/src/components/portal/plot.jsx
new file mode 100644
index 0000000..50e03f1
--- /dev/null
+++ b/src/components/portal/plot.jsx
@@ -0,0 +1,225 @@
+'use client';
+/* My Plot */
+import React, { useState, useRef } from 'react';
+import Icon from './icons';
+import { PageHead, StatusBadge } from './ui';
+import { SectorMap } from './map';
+import { useUser } from './user-store';
+import YEIDA from './data';
+
+const D = YEIDA, fmt = D.fmtINR, P = D.property;
+
+ // Address verification — NO OTP. Two accepted methods:
+ // 1) Postcard: the Authority mails a code to the address (Google-Business style).
+ // 2) Address document: upload an address proof (electricity/water bill, house tax,
+ // bank statement, driving licence or passport).
+ function PostcardVerify() {
+ const [stage, setStage] = useState('idle'); // idle | post | doc | verified
+ const [code, setCode] = useState('');
+ const [fileName, setFileName] = useState('');
+ const fileRef = useRef(null);
+
+ if (stage === 'verified') {
+ return Address verified ;
+ }
+ if (stage === 'post') {
+ return (
+
+
A postcard with a 6-digit code has been dispatched — it usually arrives in 5–7 working days. (No OTP is used for address verification.)
+
+
setCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="Enter code from postcard" />
+
setStage('verified')}>Confirm
+
+
+ setCode('')} style={{ background: 'none', border: 'none', color: 'var(--blue-600)', fontWeight: 600, fontSize: 11.5, cursor: 'pointer', padding: 0 }}>Didn't get it? Request a new postcard
+ setStage('idle')} style={{ background: 'none', border: 'none', color: 'var(--muted)', fontWeight: 600, fontSize: 11.5, cursor: 'pointer', padding: 0 }}>Cancel
+
+
+ );
+ }
+ if (stage === 'doc') {
+ return (
+
+
Upload an address proof — electricity/water bill, house tax receipt, bank statement, driving licence or passport.
+
{ const f = e.target.files && e.target.files[0]; if (f) setFileName(f.name); }} />
+
+ fileRef.current && fileRef.current.click()}> {fileName || 'Choose document'}
+ setStage('verified')}>Submit
+
+
setStage('idle')} style={{ background: 'none', border: 'none', color: 'var(--muted)', fontWeight: 600, fontSize: 11.5, cursor: 'pointer', padding: 0, marginTop: 8 }}>Cancel
+
+ );
+ }
+ return (
+
+ setStage('post')}> Verify by post
+ setStage('doc')}> Upload address document
+
+ );
+ }
+
+ function AddressRow({ title, sub, addr, icon, verifyNode, badge }) {
+ return (
+
+
+
{sub}
+
{addr.line1}, {addr.line2} {addr.city}, {addr.state} — {addr.pin}
+ {verifyNode &&
{verifyNode}
}
+
+ );
+ }
+
+ const timeline = [
+ { title: 'Plot Allotted', date: '14 Mar 2019', desc: 'Allotment letter issued for Plot B-47', color: 'emerald', icon: 'check' },
+ { title: 'Possession Granted', date: '22 Aug 2019', desc: 'Physical possession handed over', color: 'emerald', icon: 'home' },
+ { title: 'Lease Deed Registered', date: '05 Sep 2019', desc: '90-year lease registered at Sub-Registrar', color: 'emerald', icon: 'file' },
+ { title: 'Building Plan Sanctioned', date: '18 Feb 2026', desc: 'Construction approval granted', color: 'blue', icon: 'construction' },
+ { title: 'Construction In Progress', date: 'Ongoing', desc: 'Ground floor structure — 65% complete', color: 'amber', icon: 'building' },
+ ];
+
+ function InfoRow({ label, value, strong }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+ }
+
+ function MyPlot({ go }) {
+ const user = useUser(D.user);
+ const allotteeName = user.isAllottee ? user.name : `${user.allotteeFirstName || ''} ${user.allotteeLastName || ''}`.trim();
+ return (
+
+
+ go('documents')}> Documents
+ go('construction')}> Construction
+
+
+ {/* hero property card */}
+
+
+
+
+
+
+
+
Plot {P.plot}, {P.sector}
+
Allotment {P.allotmentNo}
+
+
+
+
+
+ {[['Plot Size', P.size], ['Category', P.category], ['Facing', P.facing], ['Block', P.block], ['Road Width', P.road], ['Lease Period', P.leasePeriod]].map(([l, v]) => (
+
+ ))}
+
+
+
+
Total Premium Value
+
{fmt(P.totalValue)}
+
+
+ {fmt(P.paid)} paid
+ {fmt(P.totalValue - P.paid)} balance
+
+
+ Allotted {P.allottedOn}
+ {P.rate}
+
+
+
+
+
+ {/* map + info */}
+
+
+
+
Plot Location
+ {P.sector}, Yamuna Expressway, Gautam Buddh Nagar
+
+
+
+
+
Ownership Information
+
+
+
+
+
+
+
+
+ go('transfer')}> Transfer Plot Ownership
+
+
+
+ {/* authority status + addresses */}
+
+
+
Authority Status
+
+ {[['Lease Deed', 'LD_COMPLETED', 'file'], ['ATL (Allotment Transfer Letter)', 'ATL_PENDING', 'checkCircle'], ['Authority Verification', 'VERIFIED_AUTH', 'shield'], ['Possession', 'POSS_GRANTED', 'home']].map(([l, code, ic]) => (
+
+ {l}
+
+
+ ))}
+
+
+
+
+
Addresses
+ go('profile')}> Edit
+
+
Authority record}
+ />
+
+ Unverified}
+ verifyNode={ }
+ />
+ Legal notices are sent to your current mailing address. Address verification uses a postcard code or an address document — not OTP.
+
+
+
+ {/* timeline */}
+
+
Property Timeline
+
+
+ {timeline.map((t, i) => {
+ const cc = { emerald: 'var(--emerald-500)', blue: 'var(--blue-500)', amber: 'var(--amber-500)' }[t.color];
+ return (
+
+
+
{t.title}
+
{t.date}
+
{t.desc}
+
+ );
+ })}
+
+
+
+ );
+ }
+
+export { MyPlot };
+export default MyPlot;
diff --git a/src/components/portal/portal-context.jsx b/src/components/portal/portal-context.jsx
new file mode 100644
index 0000000..4530274
--- /dev/null
+++ b/src/components/portal/portal-context.jsx
@@ -0,0 +1,18 @@
+'use client';
+/* Provides `go` (navigation) and `toast` to every authenticated portal page,
+ so individual page components keep their original `{ go, toast }` props. */
+import React, { createContext, useContext } from 'react';
+
+const PortalContext = createContext({ go: () => {}, toast: () => {} });
+
+export function PortalProvider({ go, toast, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePortal() {
+ return useContext(PortalContext);
+}
diff --git a/src/components/portal/portal.css b/src/components/portal/portal.css
new file mode 100644
index 0000000..b3dcf1c
--- /dev/null
+++ b/src/components/portal/portal.css
@@ -0,0 +1,274 @@
+/* ============================================================
+ YEIDA Portal — Design System
+ Premium government-tech: navy / blue / emerald on clean white
+ ============================================================ */
+
+/* Same font families as the main website (Heebo / Plus Jakarta Sans / Parkinsans / Libre Baskerville) */
+@import url("https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Heebo:wght@300;400;500;600;700&family=Parkinsans:wght@300..800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Philosopher:wght@400;700&family=Gloock&display=swap");
+
+:root {
+ /* Brand */
+ --navy-900: #081a39;
+ --navy-800: #0b2548;
+ --navy-700: #103263;
+ --navy-600: #16407e;
+ --blue-600: #1d5fbf;
+ --blue-700: #16407e;
+ --blue-500: #2670e6;
+ --blue-400: #5a93f0;
+ --blue-100: #d8e6fb;
+ --blue-50: #eef4ff;
+ --emerald-700: #0a7d56;
+ --emerald-600: #0f9d6b;
+ --emerald-500: #16b67e;
+ --emerald-100: #cdeede;
+ --emerald-50: #e8f7f0;
+ --amber-600: #c77a08;
+ --amber-500: #e8930c;
+ --amber-50: #fdf3e2;
+ --red-600: #d23f3f;
+ --red-500: #e85656;
+ --red-50: #fdedee;
+
+ /* Neutrals */
+ --ink: #0e1b2e;
+ --ink-2: #2b3a4f;
+ --muted: #5d6f87;
+ --faint: #8a99 ac;
+ --faint: #8a99ac;
+ --line: #e4eaf1;
+ --line-2: #eef2f7;
+ --bg: #f4f7fb;
+ --bg-2: #eef3f9;
+ --card: #ffffff;
+ --ink-on-navy: #eaf1fb;
+
+ /* Type — matched to the main website */
+ --font-display: 'Parkinsans', 'Heebo', system-ui, sans-serif;
+ --font-ui: 'Plus Jakarta Sans', 'Heebo', system-ui, -apple-system, sans-serif;
+
+ /* Radii */
+ --r-sm: 8px;
+ --r-md: 11px;
+ --r-lg: 16px;
+ --r-xl: 22px;
+ --r-pill: 999px;
+
+ /* Shadows */
+ --sh-xs: 0 1px 2px rgba(13,38,76,.05);
+ --sh-sm: 0 1px 2px rgba(13,38,76,.05), 0 4px 12px -4px rgba(13,38,76,.06);
+ --sh-md: 0 8px 24px -8px rgba(13,38,76,.13), 0 2px 6px -2px rgba(13,38,76,.05);
+ --sh-lg: 0 24px 48px -16px rgba(11,37,72,.26), 0 6px 16px -8px rgba(11,37,72,.16);
+ --sh-glow: 0 10px 28px -8px rgba(29,95,191,.42);
+
+ --sidebar-w: 252px;
+ --topbar-h: 64px;
+}
+
+* { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+body {
+ font-family: var(--font-ui);
+ color: var(--ink);
+ background: var(--bg);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+#root { min-height: 100vh; }
+
+h1,h2,h3,h4,h5 { font-family: var(--font-display); margin: 0; color: var(--ink); letter-spacing: -.015em; }
+p { margin: 0; }
+button { font-family: inherit; cursor: pointer; }
+a { color: inherit; text-decoration: none; }
+input, select, textarea { font-family: inherit; }
+::selection { background: var(--blue-100); }
+
+.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
+
+/* Scrollbars */
+*::-webkit-scrollbar { width: 10px; height: 10px; }
+*::-webkit-scrollbar-thumb { background: #cdd7e4; border-radius: 99px; border: 2px solid transparent; background-clip: padding-box; }
+*::-webkit-scrollbar-thumb:hover { background: #b4c2d4; background-clip: padding-box; }
+*::-webkit-scrollbar-track { background: transparent; }
+
+/* ---------- Buttons ---------- */
+.btn {
+ display: inline-flex; align-items: center; justify-content: center; gap: 9px;
+ font-family: var(--font-ui); font-weight: 600; font-size: 14.5px;
+ padding: 11px 18px; border-radius: var(--r-md); border: 1px solid transparent;
+ background: transparent; color: var(--ink); white-space: nowrap;
+ transition: transform .12s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease, color .15s ease;
+ line-height: 1;
+}
+.btn:active { transform: translateY(1px); }
+.btn svg { width: 18px; height: 18px; flex: none; }
+
+.btn-primary {
+ background: #0077FF;
+ color: #fff; box-shadow: 0 10px 24px -8px rgba(0,119,255,.5);
+ border-radius: var(--r-pill) !important;
+}
+.btn-primary:hover { box-shadow: 0 14px 32px -8px rgba(0,119,255,.6); filter: brightness(1.05); }
+
+.btn-navy { background: linear-gradient(180deg, var(--navy-700), var(--navy-800)); color: #fff; box-shadow: 0 10px 24px -10px rgba(8,26,57,.6); }
+.btn-navy:hover { filter: brightness(1.1); }
+
+.btn-emerald { background: linear-gradient(180deg, var(--emerald-500), var(--emerald-600)); color: #fff; box-shadow: 0 10px 26px -10px rgba(15,157,107,.55); }
+.btn-emerald:hover { filter: brightness(1.05); }
+
+.btn-outline { background: #fff; border-color: var(--line); color: var(--ink-2); box-shadow: var(--sh-xs); }
+.btn-outline:hover { border-color: #c9d6e6; background: #fbfdff; }
+
+.btn-ghost { color: var(--muted); }
+.btn-ghost:hover { background: var(--bg-2); color: var(--ink); }
+
+.btn-sm { padding: 8px 13px; font-size: 13.5px; border-radius: var(--r-sm); }
+.btn-sm svg { width: 16px; height: 16px; }
+.btn-lg { padding: 14px 24px; font-size: 16px; }
+.btn-block { width: 100%; }
+
+/* ---------- Cards ---------- */
+.card {
+ background: var(--card); border: 1px solid var(--line);
+ border-radius: var(--r-lg); box-shadow: var(--sh-sm);
+}
+.card-pad { padding: 22px; }
+.card-hover { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; }
+.card-hover:hover { box-shadow: var(--sh-md); border-color: #d6e0ee; transform: translateY(-2px); }
+
+/* ---------- Badges / chips ---------- */
+.badge {
+ display: inline-flex; align-items: center; gap: 6px;
+ font-size: 12.5px; font-weight: 600; padding: 4px 10px; border-radius: var(--r-pill);
+ line-height: 1.3; white-space: nowrap;
+}
+.badge .dot { width: 6px; height: 6px; border-radius: 99px; background: currentColor; }
+.badge-green { background: var(--emerald-50); color: var(--emerald-700); }
+.badge-blue { background: var(--blue-50); color: var(--blue-600); }
+.badge-amber { background: var(--amber-50); color: var(--amber-600); }
+.badge-red { background: var(--red-50); color: var(--red-600); }
+.badge-gray { background: var(--bg-2); color: var(--muted); }
+.badge-navy { background: #e9eefb; color: var(--navy-700); }
+
+.chip {
+ display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
+ font-size: 13.5px; font-weight: 500; padding: 8px 14px; border-radius: var(--r-pill);
+ background: #fff; border: 1px solid var(--line); color: var(--ink-2);
+ transition: all .15s ease;
+}
+.chip:hover { border-color: #c9d6e6; background: #fbfdff; }
+.chip.active { background: var(--navy-800); color: #fff; border-color: var(--navy-800); }
+
+/* ---------- Inputs ---------- */
+.field { display: flex; flex-direction: column; gap: 7px; }
+.field label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
+.input, .select {
+ width: 100%; padding: 12px 14px; font-size: 14.5px; color: var(--ink);
+ background: #fff; border: 1.5px solid var(--line); border-radius: var(--r-md);
+ transition: border-color .15s ease, box-shadow .15s ease; outline: none;
+}
+.input::placeholder { color: #9aa9bc; }
+.input:focus, .select:focus { border-color: var(--blue-400); box-shadow: 0 0 0 4px rgba(38,112,230,.12); }
+.input-icon { position: relative; }
+.input-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #9aa9bc; }
+.input-icon .input { padding-left: 42px; }
+
+/* ---------- Sidebar nav ---------- */
+.nav-item {
+ display: flex; align-items: center; gap: 12px; width: 100%;
+ padding: 9.5px 12px; border-radius: var(--r-md); border: none; background: transparent;
+ color: #b9c7dd; font-size: 14.5px; font-weight: 500; text-align: left;
+ transition: background .15s ease, color .15s ease; position: relative;
+}
+.nav-item svg { width: 19px; height: 19px; flex: none; }
+.nav-item:hover { background: rgba(255,255,255,.06); color: #eef3fb; }
+.nav-item.active { background: rgba(255,255,255,.1); color: #fff; font-weight: 600; }
+.nav-item.active::before {
+ content: ''; position: absolute; left: -12px; top: 50%; transform: translateY(-50%);
+ width: 3.5px; height: 22px; border-radius: 99px; background: var(--emerald-500);
+}
+.nav-badge { margin-left: auto; font-size: 11px; font-weight: 700; background: var(--emerald-500); color: #042e1f; padding: 1px 7px; border-radius: 99px; }
+
+/* ---------- Misc ---------- */
+.divider { height: 1px; background: var(--line); border: none; }
+.kicker { font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--blue-600); }
+.muted { color: var(--muted); }
+.faint { color: #93a2b6; }
+
+.avatar { border-radius: 99px; object-fit: cover; background: linear-gradient(135deg,var(--blue-500),var(--navy-700)); display: inline-flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; }
+
+.progress { height: 8px; background: var(--bg-2); border-radius: 99px; overflow: hidden; }
+.progress > span { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg,var(--emerald-500),var(--emerald-600)); transition: width .6s cubic-bezier(.2,.7,.3,1); }
+
+/* Animations are OPT-IN via body.anim-on (added by JS only when the document is visible).
+ Default/hidden/print state shows content fully so nothing ever sticks at opacity:0. */
+@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
+@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
+@keyframes popIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
+@keyframes shimmer { 100% { transform: translateX(100%); } }
+@keyframes spin { to { transform: rotate(360deg); } }
+@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
+body.anim-on .anim-fade-up { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; }
+body.anim-on .anim-fade-in { animation: fadeIn .4s ease both; }
+body.anim-on .anim-pop { animation: popIn .35s cubic-bezier(.2,.8,.3,1) both; }
+body.anim-on .stagger > * { animation: fadeUp .5s cubic-bezier(.2,.7,.3,1) both; }
+.stagger > *:nth-child(1){animation-delay:.03s}
+.stagger > *:nth-child(2){animation-delay:.08s}
+.stagger > *:nth-child(3){animation-delay:.13s}
+.stagger > *:nth-child(4){animation-delay:.18s}
+.stagger > *:nth-child(5){animation-delay:.23s}
+.stagger > *:nth-child(6){animation-delay:.28s}
+.stagger > *:nth-child(7){animation-delay:.33s}
+.stagger > *:nth-child(8){animation-delay:.38s}
+
+/* Tables */
+.tbl { width: 100%; border-collapse: collapse; }
+.tbl th { text-align: left; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--faint); padding: 12px 16px; border-bottom: 1px solid var(--line); }
+.tbl td { padding: 14px 16px; border-bottom: 1px solid var(--line-2); font-size: 14px; color: var(--ink-2); }
+.tbl tbody tr { transition: background .12s ease; }
+.tbl tbody tr:hover { background: #fafcfe; }
+.tbl tbody tr:last-child td { border-bottom: none; }
+
+/* Utility layout */
+.row { display: flex; align-items: center; }
+.col { display: flex; flex-direction: column; }
+.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}.gap-6{gap:24px}
+.grow{flex:1}.center{align-items:center;justify-content:center}
+.spread{justify-content:space-between}
+
+/* Tooltip-ish */
+.icon-btn {
+ width: 38px; height: 38px; border-radius: var(--r-md); border: 1px solid transparent;
+ display: inline-flex; align-items: center; justify-content: center; background: transparent; color: var(--muted);
+ transition: background .15s, color .15s, border-color .15s; position: relative;
+}
+.icon-btn:hover { background: var(--bg-2); color: var(--ink); }
+.icon-btn svg { width: 20px; height: 20px; }
+.notif-dot { position: absolute; top: 7px; right: 8px; width: 8px; height: 8px; border-radius: 99px; background: var(--red-500); border: 2px solid #fff; }
+
+/* ---------- Dashboard polish ---------- */
+/* premium stat/status card: hairline top accent + refined hover */
+.stat-card { position: relative; overflow: hidden; }
+.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--accent, var(--blue-500)); opacity: .9; }
+.stat-card { transition: box-shadow .22s ease, transform .22s ease, border-color .22s ease; }
+.stat-card:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; }
+
+/* section eyebrow heading */
+.sec-eyebrow { display: inline-flex; align-items: center; gap: 8px; font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--blue-600); }
+.sec-eyebrow::before { content: ''; width: 22px; height: 2px; border-radius: 2px; background: var(--blue-500); }
+
+/* refined module switch card */
+.mod-switch { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease, background .2s ease; }
+.mod-switch:hover { transform: translateY(-2px); box-shadow: var(--sh-md); }
+
+/* subtle metric tile */
+.metric-tile { transition: box-shadow .2s ease, transform .2s ease, border-color .2s ease; }
+.metric-tile:hover { box-shadow: var(--sh-md); transform: translateY(-2px); border-color: #d6e0ee; }
+
+/* table refinement on dashboard */
+.tbl.tbl-clean th { background: var(--bg); }
+
+
+/* keyframes migrated from ui.jsx (bar chart) and auth.jsx (verify ping) */
+@keyframes growBar { from { transform: scaleY(0); transform-origin: bottom; } to { transform: scaleY(1); transform-origin: bottom; } }
+@keyframes ping { 0% { transform: scale(.8); opacity: .6; } 100% { transform: scale(1.8); opacity: 0; } }
diff --git a/src/components/portal/sampleplans.jsx b/src/components/portal/sampleplans.jsx
new file mode 100644
index 0000000..7ef979b
--- /dev/null
+++ b/src/components/portal/sampleplans.jsx
@@ -0,0 +1,225 @@
+'use client';
+/* Sample Plans & Virtual Tours + First-time Journey ("What would you like to do next?") */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { PageHead } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA;
+
+ /* ---------------- SAMPLE PLANS & VIRTUAL TOURS ---------------- */
+ const FILTERS = [
+ { key: 'all', label: 'All Plans' },
+ { key: 'size', label: 'My Plot Size (300 sq.m)', test: p => p.size === '300 sq.m' },
+ { key: 'facing', label: 'My Orientation (East)', test: p => p.facing === 'East' },
+ { key: 'tour', label: '3D Walkthrough', test: p => p.tour },
+ ];
+
+ function PlanThumb({ accent }) {
+ return (
+
+
+
+ {/* room partitions — schematic floor plan */}
+
+
+
+
+
+
+
+ {/* compass */}
+ N
+
+ );
+ }
+
+ // Front-facade schematic — used for elevation variants
+ function ElevationThumb({ accent, floors }) {
+ const two = (floors || '').includes('2');
+ return (
+
+
+ {/* ground floor */}
+
+ {/* upper floor */}
+ {two && }
+ {/* roof line */}
+
+ {/* door */}
+
+ {/* windows */}
+
+
+ {two && <> >}
+
+ );
+ }
+
+ function SamplePlans({ go, toast }) {
+ const [f, setF] = useState('all');
+ const active = FILTERS.find(x => x.key === f);
+ const list = D.samplePlans.filter(p => !active.test || active.test(p));
+ const [tour, setTour] = useState(null);
+ const [elev, setElev] = useState(D.elevations[0].id);
+ const mySize = '300 sq.m', myFacing = 'East';
+ return (
+
+
+ go('journey')}> What's next
+ go('construction')}> Start Construction
+
+
+ {/* hero strip */}
+
+
+
Plot-specific guidance
+
Plans tailored to Plot B-47
+
Based on your 300 sq.m east-facing plot in Sector 18, here are orientation- and size-appropriate designs with 3D walkthroughs and virtual tours.
+
+ {[['Plot size', '300 sq.m'], ['Facing', 'East'], ['Permissible FAR', '1.5'], ['Max height', '15 m']].map(([l, v]) => (
+
+ ))}
+
+
+
+
+
+
Immersive 3D Walkthroughs
+
Tour {D.samplePlans.filter(p => p.tour).length} designs in virtual reality
+
+
+
+
+ {/* filters */}
+
+ {FILTERS.map(x => setF(x.key)}>{x.label} )}
+
+
+ {/* plan grid */}
+
+ {list.map(p => (
+
+
+
+ {p.tour &&
setTour(p)} style={{ position: 'absolute', top: 12, right: 12, display: 'flex', alignItems: 'center', gap: 6, padding: '6px 11px', borderRadius: 99, border: 'none', background: 'rgba(8,26,57,.78)', color: '#fff', fontSize: 12, fontWeight: 600, cursor: 'pointer', backdropFilter: 'blur(4px)' }}> 3D Tour }
+
+
+
+
{p.name}
+ {p.style}
+
+
+ {p.size}
+ {p.beds} BHK
+ {p.floors}
+
+
+ setTour(p)}> View Plan
+ toast && toast('Plan downloaded')}>
+
+
+
+ ))}
+
+
+ {/* ---- Elevation variants (front facade) — vary by plot size & orientation ---- */}
+
+
+
+
Elevation Variants
+
Front-facade options matched to your {mySize} · {myFacing}-facing plot. The elevation follows your selected plan & orientation.
+
+
{D.elevations.filter(e => e.suitsSize === mySize || e.suitsFacing === myFacing).length} recommended
+
+
+ {D.elevations.map(e => {
+ const on = elev === e.id;
+ const fits = e.suitsSize === mySize || e.suitsFacing === myFacing;
+ return (
+
setElev(e.id)} className="card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: 0, overflow: 'hidden', borderRadius: 14, border: '1.5px solid ' + (on ? 'var(--blue-400)' : 'var(--line)'), background: '#fff', boxShadow: on ? '0 0 0 4px rgba(38,112,230,.1)' : 'none' }}>
+
+
+ {fits && Recommended }
+ {on && }
+
+
+
{e.name} {e.floors}
+
{e.desc}
+
Suits {e.suitsSize} · {e.suitsFacing}-facing
+
+
+ );
+ })}
+
+
+ toast && toast('Elevation applied to your plan')}> Apply elevation
+ { const p = D.samplePlans.find(x => x.tour) || D.samplePlans[0]; setTour(p); }}> View in 3D walkthrough
+
+
+
+ {tour &&
setTour(null)} />}
+
+ );
+ }
+
+ function TourModal({ plan, onClose }) {
+ return (
+
+
e.stopPropagation()} className="anim-pop card" style={{ width: 760, maxWidth: '100%', padding: 0, overflow: 'hidden' }}>
+
+
{plan.name}
{plan.style} · {plan.size} · {plan.facing} facing
+
+
+
+
+
+
+
3D Virtual Walkthrough
+
Immersive tour placeholder — drop in your Matterport / 360° embed
+
+
+
+
{['Living', 'Bedrooms', 'Kitchen', 'Terrace'].map((r, i) => {r} )}
+
Download Plan
+
+
+
+ );
+ }
+
+ /* ---------------- JOURNEY: "What would you like to do?" (7 action cards) ---------------- */
+ function Journey({ go }) {
+ const COL = { blue: ['var(--blue-50)', 'var(--blue-600)'], navy: ['#e9eefb', 'var(--navy-700)'], amber: ['var(--amber-50)', 'var(--amber-600)'], emerald: ['var(--emerald-50)', 'var(--emerald-700)'] };
+ return (
+
+
+
+
What would you like to do?
+
Your plot is verified, {D.user.firstName}. Choose an action to get started — you can always come back here.
+
+
+
+ {D.onboardingActions.map((c, i) => {
+ const cc = COL[c.color];
+ const wide = i === 6; // last card spans for 7-card balance
+ return (
+
go(c.route)} className="card card-hover" style={{ textAlign: 'left', cursor: 'pointer', padding: '24px 22px', border: '1px solid var(--line)', gridColumn: wide ? '2 / 3' : undefined, display: 'flex', flexDirection: 'column', gap: 14 }}>
+
+
+
+ );
+ })}
+
+
+
+ go('dashboard')}>Skip to dashboard
+
+
+ );
+ }
+
+export { SamplePlans, Journey };
diff --git a/src/components/portal/shell.jsx b/src/components/portal/shell.jsx
new file mode 100644
index 0000000..a948954
--- /dev/null
+++ b/src/components/portal/shell.jsx
@@ -0,0 +1,129 @@
+'use client';
+/* App shell: sidebar + topbar. Renders children content. */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { Logo, Avatar } from './ui';
+import YEIDA from './data';
+import { FloatingAssistant } from './floating-assistant';
+import { useLang, LangToggle } from '../i18n';
+import { useUserPhoto, useUser } from './user-store';
+
+const D = YEIDA;
+
+ const NAV = [
+ { k: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
+ { k: 'plot', label: 'My Plot', icon: 'plot' },
+ { k: 'myjourney', label: 'My Journey', icon: 'flag' },
+ { k: 'sampleplans', label: 'Sample Plans', icon: 'grid' },
+ { k: 'construction', label: 'Construction', icon: 'construction' },
+ { k: 'payments', label: 'Payments', icon: 'payments', badge: '1' },
+ { k: 'documents', label: 'Documents', icon: 'documents' },
+ { k: 'notices', label: 'Notices', icon: 'notices', badge: '2' },
+ { k: 'sectormaps', label: 'Sector Maps', icon: 'map' },
+ { k: 'transfer', label: 'Transfer Plot', icon: 'transfer' },
+ { k: 'assistant', label: 'AI Assistant', icon: 'assistant' },
+ ];
+ const NAV2 = [
+ { k: 'support', label: 'Support', icon: 'support' },
+ { k: 'profile', label: 'Profile', icon: 'profile' },
+ ];
+
+ function Shell({ route, go, children, dark, setDark }) {
+ const [q, setQ] = useState('');
+ const { t } = useLang();
+ const [photo] = useUserPhoto();
+ const user = useUser(D.user);
+ const titleMap = {
+ dashboard: 'My Plot Dashboard', plot: 'My Plot', construction: 'Construction',
+ payments: 'Payments', documents: 'Document Center', notices: 'Notices & Announcements',
+ sectormaps: 'Sector Maps', transfer: 'Transfer Plot', assistant: 'AI Assistant',
+ support: 'Support Center', profile: 'Profile', sampleplans: 'Sample Plans & Virtual Tours',
+ journey: 'Get Started', myjourney: 'My Construction Journey',
+ };
+ return (
+
+ {/* ---- Sidebar ---- */}
+
+
+
+ {/* property capsule */}
+
+
+
+
+
+
+
Plot {D.user.plot}, {D.user.sector}
+
{D.user.size} · {D.user.category}
+
+
+
+
+
+ {NAV.map(n => (
+ go(n.k)}>
+
+ {t(n.label)}
+ {n.badge && {n.badge} }
+
+ ))}
+
+ {NAV2.map(n => (
+ go(n.k)}>
+ {t(n.label)}
+
+ ))}
+
+
+ go('welcome')}>
+ {t('Logout')}
+
+
+
+ {/* ---- Main ---- */}
+
+ {/* topbar */}
+
+
+
+ {children}
+
+
+ {route !== 'assistant' &&
}
+
+ );
+ }
+
+export { Shell };
+export default Shell;
diff --git a/src/components/portal/site-header.css b/src/components/portal/site-header.css
new file mode 100644
index 0000000..12ce2c5
--- /dev/null
+++ b/src/components/portal/site-header.css
@@ -0,0 +1,102 @@
+/* Marketing site header styles ONLY — extracted from (site)/style.css so the
+ website can be shown on the portal login page without pulling in
+ the rest of the marketing stylesheet. Font vars are scoped to the header. */
+.site-header {
+ --display: "Parkinsans", "Heebo", sans-serif;
+ --ink: #151515;
+ z-index: 50;
+ padding: 18px 0;
+ position: absolute;
+ top: 0px;
+ left: 0;
+ right: 0;
+ background: #0077ff;
+}
+.header-inner {
+ max-width: 1835px;
+ margin: 0 auto;
+ padding: 0 50px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+}
+.brand { display: flex; align-items: center; gap: 12px; color: #fff; }
+.brand-emblem {
+ width: 54px; height: 54px; border-radius: 50%;
+ display: grid; place-items: center;
+ background: rgba(255, 255, 255, 0.12);
+ overflow: hidden;
+}
+.brand-emblem img { width: 100%; height: 100%; object-fit: cover; }
+.brand-text {
+ font-family: var(--display);
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 1.05;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+.nav-pill {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px;
+ border-radius: 100px;
+ background: rgba(255, 255, 255, 0.71);
+ border: 1px solid #FFFFFF;
+ backdrop-filter: blur(100px);
+ border-radius: 100px;
+}
+.nav-link {
+ font-family: var(--display);
+ font-size: 15px;
+ color: #1d2430;
+ padding: 11px 20px;
+ border-radius: 100px;
+ white-space: nowrap;
+ transition: background 0.2s ease, color 0.2s ease;
+}
+.nav-link:hover { background: #eef2f7; }
+.nav-link.active { background: white; color: #000000; }
+.header-right { display: flex; align-items: center; gap: 14px; }
+.social-dots { display: flex; gap: 8px; }
+.social-dots .dot {
+ width: 38px; height: 38px; border-radius: 50%;
+ display: grid; place-items: center;
+ background: rgba(255, 255, 255, 0.16);
+ color: #fff; font-size: 13px;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ transition: background 0.2s ease;
+}
+.social-dots .dot:hover { background: rgba(255, 255, 255, 0.32); }
+.social-dots .dot img { width: 18px; height: 18px; object-fit: contain; }
+.btn-login {
+ font-family: var(--display);
+ background: #fff; color: var(--ink);
+ padding: 11px 26px; border-radius: 100px; font-size: 15px;
+ transition: transform 0.2s ease;
+}
+.btn-login:hover { transform: translateY(-2px); }
+.menu-btn {
+ width: 46px; height: 46px; border-radius: 14px;
+ background: #fff; display: none;
+ flex-direction: column; gap: 4px; align-items: center; justify-content: center;
+}
+.menu-btn span { width: 18px; height: 2px; background: var(--ink); display: block; }
+
+@media (max-width: 1024px) {
+ .nav-pill {
+ position: absolute;
+ top: 88px; right: 50px;
+ flex-direction: column;
+ align-items: stretch;
+ display: none;
+ }
+ .nav-pill.is-open { display: flex; }
+ .menu-btn { display: flex; }
+ .social-dots { display: none; }
+}
+@media (max-width: 768px) {
+ .header-inner { padding: 0 20px; }
+}
diff --git a/src/components/portal/transfer.jsx b/src/components/portal/transfer.jsx
new file mode 100644
index 0000000..b6ae252
--- /dev/null
+++ b/src/components/portal/transfer.jsx
@@ -0,0 +1,211 @@
+'use client';
+/* Transfer Plot module */
+import React, { useState } from 'react';
+import Icon from './icons';
+import { PageHead } from './ui';
+import YEIDA from './data';
+
+const D = YEIDA, fmt = D.fmtINR;
+
+ const STEPS = ['Eligibility', 'Requirements', 'Fee Calculator', 'Application', 'Review'];
+
+ function Transfer({ go, toast }) {
+ const [step, setStep] = useState(0);
+ const feeTotal = D.transferFees.reduce((s, f) => s + f.amount, 0);
+
+ return (
+
+
+
+ {/* progress */}
+
+ {STEPS.map((s, i) => (
+
+ setStep(i)} className="row gap-2" style={{ background: 'none', border: 'none', cursor: 'pointer', flex: 'none' }}>
+ {i < step ? : i + 1}
+ {s}
+
+ {i < STEPS.length - 1 && }
+
+ ))}
+
+
+
+ {step === 0 &&
setStep(1)} />}
+ {step === 1 && }
+ {step === 2 && }
+ {step === 3 && }
+ {step === 4 && }
+
+
+
+ setStep(s => Math.max(0, s - 1))}> Back
+ {step < 4 && setStep(s => s + 1)}>Continue }
+
+
+ );
+ }
+
+ function Eligibility({ next }) {
+ const checks = [
+ ['5-year lock-in period completed', true, 'Allotted Mar 2019 · 7 years held'],
+ ['No outstanding transfer restrictions', true, 'Clear'],
+ ['Lease deed registered', true, 'Registered Sep 2019'],
+ ['Pending dues cleared before transfer', false, '₹1,47,585 to be cleared first'],
+ ];
+ const eligible = true;
+ return (
+
+
+
Eligibility Check
+
We verified your plot against transfer policy 2026.
+
+ {checks.map(([l, ok, note], i) => (
+
+
+
{ok ? 'Passed' : 'Action needed'}
+
+ ))}
+
+
+
+
+
You're eligible to transfer
+
Your plot meets the core eligibility criteria. Clear pending dues to proceed to fee payment and application.
+
Proceed to Requirements
+
+
+ );
+ }
+
+ function Requirements() {
+ const reqs = [
+ ['Transferor (Seller) Documents', ['PAN & Aadhaar', 'Original allotment letter', 'Latest dues clearance', 'No-objection affidavit']],
+ ['Transferee (Buyer) Documents', ['PAN & Aadhaar', 'Photographs (2)', 'Address proof', 'Acceptance undertaking']],
+ ['Joint / Common', ['Registered transfer deed', 'Sale agreement', 'Stamp duty receipt', 'Transfer fee challan']],
+ ];
+ return (
+
+ {reqs.map(([title, items], i) => (
+
+
{title}
+
+ {items.map(it =>
{it}
)}
+
+
+ ))}
+
+ );
+ }
+
+ function FeeCalc({ fees, total }) {
+ const [value, setValue] = useState(7725000);
+ const transfer = Math.round(value * 0.025);
+ const computed = transfer + 5000 + 2500 + 1350;
+ return (
+
+
+
Fee Calculator
+
+
Declared Transfer Value
+
setValue(+e.target.value || 0)} />
+
setValue(+e.target.value)} style={{ accentColor: 'var(--blue-600)', marginTop: 10 }} />
+
₹50 L ₹1.2 Cr
+
+
+
Transfer Charges (2.5%)
+
{fmt(transfer)}
+
+
+
+
Fee Breakdown
+
+ {[['Transfer Charges (2.5% of value)', transfer], ['Processing Fee', 5000], ['Documentation Charges', 2500], ['GST (18% on fees)', 1350]].map(([l, a], i) => (
+
+ {l} {fmt(a)}
+
+ ))}
+
+ Total Payable
+ {fmt(computed)}
+
+
+
Generate Transfer Challan
+
+
+ );
+ }
+
+ function Application() {
+ const fields = [
+ ['Transferee Full Name', 'user', 'Enter buyer name'],
+ ['Transferee Mobile', 'phone', '+91'],
+ ['Transferee Email', 'mail', 'buyer@email.com'],
+ ['Transferee PAN', 'file', 'ABCDE1234F'],
+ ['Relationship to Transferor', 'user', 'e.g. Third party'],
+ ['Reason for Transfer', 'info', 'e.g. Sale'],
+ ];
+ return (
+
+
+
Transfer Application
+
+ {fields.map(([l, ic, ph]) => (
+
+ ))}
+
+
+
+
Upload Documents
+
+
+
Upload transfer deed & KYC
+
PDF · up to 25 MB
+
+ {['Transfer Deed', 'Buyer KYC', 'Sale Agreement'].map(d => (
+
+ {d}
+ Upload
+
+ ))}
+
+
+ );
+ }
+
+ function Review({ total, go, toast }) {
+ const [submitted, setSubmitted] = useState(false);
+ if (submitted) return (
+
+
+
Transfer application submitted
+
Application TR-2026-18-0472 is now with the Authority for review. You can track approval status from your dashboard.
+
go('dashboard')}>Go to Dashboard go('documents')}>View Documents
+
+ );
+ return (
+
+
+
Review & Submit
+ {[['Plot', 'B-47, Sector 18'], ['Transferor', 'Rajeev Malhotra'], ['Transferee', 'New Allottee'], ['Transfer Value', fmt(7725000)], ['Total Fees', fmt(201975)], ['Documents', '3 uploaded']].map(([l, v]) => (
+
{l} {v}
+ ))}
+
I declare the above information is true and complete.
+
+
+
Approval Tracking
+
After submission your application moves through:
+ {['Document verification', 'Dues clearance check', 'Authority approval', 'Transfer completion'].map((s, i) => (
+
{i + 1} {s}
+ ))}
+
{ setSubmitted(true); toast && toast('Transfer application submitted'); }}>Submit Application
+
+
+ );
+ }
+
+export { Transfer };
+export default Transfer;
diff --git a/src/components/portal/ui.jsx b/src/components/portal/ui.jsx
new file mode 100644
index 0000000..69e31b7
--- /dev/null
+++ b/src/components/portal/ui.jsx
@@ -0,0 +1,146 @@
+'use client';
+/* Shared UI primitives — Logo, charts, helpers. */
+import React, { useState, useEffect, useRef } from 'react';
+import Icon from './icons';
+import YEIDA from './data';
+import { useLang } from '../i18n';
+
+ /* YEIDA logo mark */
+ function Logo({ size = 38, light = false, showText = true, sub = true }) {
+ const fg = light ? '#fff' : 'var(--navy-800)';
+ return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 11 } },
+ React.createElement('img', {
+ src: '/yeida-logo.png', alt: 'YEIDA',
+ style: {
+ width: size * 1.08, height: size * 1.08, flex: 'none', objectFit: 'contain', display: 'block',
+ filter: light ? 'drop-shadow(0 2px 6px rgba(0,0,0,.35))' : 'drop-shadow(0 2px 5px rgba(16,49,99,.25))',
+ }
+ }),
+ showText && React.createElement('div', { style: { lineHeight: 1.05 } },
+ React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.46, color: fg, letterSpacing: '-.01em' } }, 'YEIDA'),
+ sub && React.createElement('div', { style: { fontSize: size * 0.235, fontWeight: 600, color: light ? 'rgba(234,241,251,.7)' : 'var(--muted)', letterSpacing: '.02em', marginTop: 1 } }, 'Yamuna Expressway Authority')
+ )
+ );
+ }
+
+ /* Avatar — renders an image when `src` is given, otherwise initials */
+ function Avatar({ initials = 'RM', size = 38, style, src }) {
+ if (src) {
+ return React.createElement('img', {
+ src, alt: '', className: 'avatar',
+ style: { width: size, height: size, objectFit: 'cover', display: 'block', ...style }
+ });
+ }
+ return React.createElement('div', {
+ className: 'avatar',
+ style: { width: size, height: size, fontSize: size * 0.4, ...style }
+ }, initials);
+ }
+
+ /* Line chart with gradient fill + animated draw */
+ function LineChart({ data, w = 520, h = 150, color = 'var(--blue-500)', fill = 'rgba(38,112,230,.12)', pad = 8 }) {
+ const max = Math.max(...data) * 1.12, min = Math.min(...data) * 0.85;
+ const range = max - min || 1;
+ const stepX = (w - pad * 2) / (data.length - 1);
+ const pts = data.map((d, i) => [pad + i * stepX, pad + (h - pad * 2) * (1 - (d - min) / range)]);
+ const line = pts.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(' ');
+ const area = `${line} L${pts[pts.length - 1][0]},${h - pad} L${pts[0][0]},${h - pad} Z`;
+ const ref = useRef(null);
+ useEffect(() => {
+ if (ref.current) {
+ const len = ref.current.getTotalLength();
+ ref.current.style.strokeDasharray = len;
+ ref.current.style.strokeDashoffset = len;
+ ref.current.getBoundingClientRect();
+ ref.current.style.transition = 'stroke-dashoffset 1.1s cubic-bezier(.3,.7,.3,1)';
+ ref.current.style.strokeDashoffset = 0;
+ }
+ }, []);
+ return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, preserveAspectRatio: 'none', style: { display: 'block' } },
+ React.createElement('path', { d: area, fill }),
+ React.createElement('path', { ref, d: line, fill: 'none', stroke: color, strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round' }),
+ pts.map((p, i) => React.createElement('circle', { key: i, cx: p[0], cy: p[1], r: i === pts.length - 1 ? 4 : 0, fill: '#fff', stroke: color, strokeWidth: 2.5 }))
+ );
+ }
+
+ /* Mini bar chart */
+ function BarChart({ data, w = 240, h = 70, color = 'var(--emerald-500)', gap = 5 }) {
+ const max = Math.max(...data) || 1;
+ const bw = (w - gap * (data.length - 1)) / data.length;
+ return React.createElement('svg', { viewBox: `0 0 ${w} ${h}`, width: '100%', height: h, style: { display: 'block' } },
+ data.map((d, i) => {
+ const bh = Math.max(3, (d / max) * h);
+ return React.createElement('rect', {
+ key: i, x: i * (bw + gap), y: h - bh, width: bw, height: bh, rx: 3,
+ fill: i === data.length - 1 ? color : 'var(--blue-100)',
+ style: { animation: `growBar .6s ${i * 0.05}s both` }
+ });
+ })
+ );
+ }
+
+ /* Donut progress */
+ function Donut({ value, size = 116, stroke = 12, color = 'var(--emerald-500)', track = 'var(--bg-2)', label, sub }) {
+ const r = (size - stroke) / 2, c = 2 * Math.PI * r;
+ const target = c - (value / 100) * c;
+ const [off, setOff] = useState(typeof document !== 'undefined' && document.hidden ? target : c);
+ useEffect(() => { const t = setTimeout(() => setOff(target), 100); return () => clearTimeout(t); }, [target]);
+ return React.createElement('div', { style: { position: 'relative', width: size, height: size } },
+ React.createElement('svg', { width: size, height: size, style: { transform: 'rotate(-90deg)' } },
+ React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: track, strokeWidth: stroke }),
+ React.createElement('circle', { cx: size / 2, cy: size / 2, r, fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeDasharray: c, strokeDashoffset: off, style: { transition: 'stroke-dashoffset 1s cubic-bezier(.3,.7,.3,1)' } })
+ ),
+ React.createElement('div', { style: { position: 'absolute', inset: 0, display: 'grid', placeItems: 'center', textAlign: 'center' } },
+ React.createElement('div', null,
+ React.createElement('div', { style: { fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: size * 0.26, color: 'var(--ink)' } }, label != null ? label : value + '%'),
+ sub && React.createElement('div', { style: { fontSize: 11.5, color: 'var(--muted)', fontWeight: 600, marginTop: 1 } }, sub)
+ )
+ )
+ );
+ }
+
+ /* Section header with kicker — auto-translates title/subtitle via the language layer */
+ function PageHead({ title, subtitle, children }) {
+ const { t } = useLang();
+ return React.createElement('div', { className: 'row spread', style: { marginBottom: 24, gap: 16, flexWrap: 'wrap' } },
+ React.createElement('div', null,
+ React.createElement('h1', { style: { fontSize: 27, fontWeight: 800 } }, t(title)),
+ subtitle && React.createElement('p', { className: 'muted', style: { fontSize: 14.5, marginTop: 5 } }, t(subtitle))
+ ),
+ children && React.createElement('div', { className: 'row gap-3' }, children)
+ );
+ }
+
+ /* Toast host */
+ function useToast() {
+ const [toasts, setToasts] = useState([]);
+ const push = (msg, kind = 'success') => {
+ const id = Math.random();
+ setToasts(t => [...t, { id, msg, kind }]);
+ setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200);
+ };
+ const node = React.createElement('div', { style: { position: 'fixed', bottom: 24, right: 24, display: 'flex', flexDirection: 'column', gap: 10, zIndex: 9000 } },
+ toasts.map(t => React.createElement('div', {
+ key: t.id, className: 'anim-pop',
+ style: {
+ display: 'flex', alignItems: 'center', gap: 11, padding: '13px 17px', borderRadius: 12,
+ background: 'var(--navy-800)', color: '#fff', boxShadow: 'var(--sh-lg)', fontSize: 14, fontWeight: 500, maxWidth: 360,
+ }
+ },
+ React.createElement('span', { style: { display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 99, background: t.kind === 'success' ? 'var(--emerald-500)' : 'var(--blue-500)', flex: 'none' } },
+ React.createElement(Icon, { name: t.kind === 'success' ? 'check' : 'info', size: 14 })),
+ t.msg
+ ))
+ );
+ return [node, push];
+ }
+
+ /* Authority status badge — maps a backend status code to a labelled, coloured chip */
+ function StatusBadge({ code, size, withIcon = true }) {
+ const D = YEIDA;
+ const s = (D && D.STATUS_CODES && D.STATUS_CODES[code]) || { label: code, tone: 'gray', icon: 'info' };
+ return React.createElement('span', { className: 'badge badge-' + s.tone, style: size === 'lg' ? { fontSize: 13, padding: '6px 12px' } : undefined },
+ withIcon && React.createElement(Icon, { name: s.icon, size: 13 }), s.label);
+ }
+
+export { Logo, Avatar, LineChart, BarChart, Donut, PageHead, useToast, StatusBadge };
diff --git a/src/components/portal/user-store.js b/src/components/portal/user-store.js
new file mode 100644
index 0000000..72e3b93
--- /dev/null
+++ b/src/components/portal/user-store.js
@@ -0,0 +1,79 @@
+'use client';
+/* Tiny client-side store for the user's profile photo (data URL).
+ Shared across registration and the authenticated app, backed by localStorage
+ so it survives the route change from /portal/register into the portal. */
+import { useSyncExternalStore } from 'react';
+
+const KEY = 'yeida_user_photo';
+let mem;
+const subs = new Set();
+
+export function getUserPhoto() {
+ if (mem !== undefined) return mem;
+ try { mem = (typeof window !== 'undefined' && window.localStorage.getItem(KEY)) || null; }
+ catch { mem = null; }
+ return mem;
+}
+
+export function setUserPhoto(dataUrl) {
+ mem = dataUrl || null;
+ try {
+ if (typeof window !== 'undefined') {
+ if (mem) window.localStorage.setItem(KEY, mem);
+ else window.localStorage.removeItem(KEY);
+ }
+ } catch { /* ignore quota / privacy-mode errors */ }
+ subs.forEach(fn => fn(mem));
+}
+
+export function subscribeUserPhoto(fn) { subs.add(fn); return () => subs.delete(fn); }
+
+/* Read a File into a data URL (for handlers). */
+export function readImageFile(file, cb) {
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => cb(reader.result);
+ reader.readAsDataURL(file);
+}
+
+/* React hook → [photo, setUserPhoto]; updates live when the photo changes anywhere. */
+export function useUserPhoto() {
+ const photo = useSyncExternalStore(subscribeUserPhoto, getUserPhoto, () => null);
+ return [photo, setUserPhoto];
+}
+
+/* -------- User profile overrides (name / relationship / allottee) --------
+ Captured at registration and applied over the static mock user, so the whole
+ portal reflects whether the account was created by the allottee or on behalf. */
+const PKEY = 'yeida_user_profile';
+let pmem;
+const psubs = new Set();
+
+export function getUserProfile() {
+ if (pmem !== undefined) return pmem;
+ try {
+ const raw = typeof window !== 'undefined' && window.localStorage.getItem(PKEY);
+ pmem = raw ? JSON.parse(raw) : null;
+ } catch { pmem = null; }
+ return pmem;
+}
+
+export function setUserProfile(patch) {
+ const next = { ...(getUserProfile() || {}), ...patch };
+ pmem = next;
+ try { if (typeof window !== 'undefined') window.localStorage.setItem(PKEY, JSON.stringify(next)); } catch { /* ignore */ }
+ psubs.forEach(fn => fn(next));
+}
+
+export function subscribeUserProfile(fn) { psubs.add(fn); return () => psubs.delete(fn); }
+
+export function useUserProfile() {
+ const p = useSyncExternalStore(subscribeUserProfile, getUserProfile, () => null);
+ return [p, setUserProfile];
+}
+
+/* Merge the registration overrides over the static mock user. */
+export function useUser(base) {
+ const [ov] = useUserProfile();
+ return ov ? { ...base, ...ov } : base;
+}