Compare commits
3 Commits
9eb234935d
...
d82d71d0b2
| Author | SHA1 | Date | |
|---|---|---|---|
| d82d71d0b2 | |||
| a459fefb7f | |||
| f634312b83 |
@@ -1,14 +1,34 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
// The marketing landing is served as static files from /public (ported verbatim
|
||||
// from lynkeduppro-landing). These rewrites reproduce the landing's vercel.json
|
||||
// clean-URL routing: "/" is the React marketing home, "/1".."/19" are the static
|
||||
// pages under /public/page. `beforeFiles` lets them override the app router's "/"
|
||||
// (which otherwise redirects to /portal/login).
|
||||
const landingRewrites: { source: string; destination: string }[] = [
|
||||
{ source: "/", destination: "/index.html" },
|
||||
{ source: "/thanks", destination: "/thanks.html" },
|
||||
{ source: "/1", destination: "/page/index.html" },
|
||||
// "/2".."/19" -> /public/page/<n>.html
|
||||
...Array.from({ length: 18 }, (_, i) => i + 2).map((n) => ({
|
||||
source: `/${n}`,
|
||||
destination: `/page/${n}.html`,
|
||||
})),
|
||||
];
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// The SDK ships ESM/TS; let Next transpile it.
|
||||
transpilePackages: ["@abe-kap/appshell-sdk"],
|
||||
// The browser calls the Shell BFF same-origin under /shell (so the HttpOnly
|
||||
// session cookie flows). We deliberately use /shell (NOT /api) to avoid
|
||||
// clobbering the existing /api/geo route. Point BFF_ORIGIN at the deployed BFF.
|
||||
async rewrites() {
|
||||
// The browser calls the Shell BFF same-origin under /shell (so the HttpOnly
|
||||
// session cookie flows). We deliberately use /shell (NOT /api) to avoid
|
||||
// clobbering the /api/* route handlers (geo, and the founder checkout).
|
||||
const bff = process.env.BFF_ORIGIN ?? "http://localhost:4000";
|
||||
return [{ source: "/shell/:path*", destination: `${bff}/api/:path*` }];
|
||||
return {
|
||||
beforeFiles: landingRewrites,
|
||||
afterFiles: [{ source: "/shell/:path*", destination: `${bff}/api/:path*` }],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* global React, ReactDOM, Nav, Hero, Manifesto, TrustMarquee, LiveMetrics, SpecSheet, CoreFeatures, ConnectedOps, MobileSection, Analytics, FieldIntelligence, Testimonials, Pricing, FinalCTA, Footer */
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
function App() {
|
||||
const glowRef = useRef(null);
|
||||
const progressRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Cursor glow follower
|
||||
const onMove = (e) => {
|
||||
if (glowRef.current) {
|
||||
glowRef.current.style.left = e.clientX + "px";
|
||||
glowRef.current.style.top = e.clientY + "px";
|
||||
}
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
|
||||
// Scroll progress
|
||||
const onScroll = () => {
|
||||
if (progressRef.current) {
|
||||
const h = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const pct = h > 0 ? (window.scrollY / h) * 100 : 0;
|
||||
progressRef.current.style.width = pct + "%";
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Build particle set once
|
||||
const particles = React.useMemo(() => {
|
||||
return Array.from({ length: 26 }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
delay: Math.random() * 18,
|
||||
dur: 18 + Math.random() * 22,
|
||||
size: 1 + Math.random() * 2.5,
|
||||
opacity: 0.3 + Math.random() * 0.6,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="bg-layer"/>
|
||||
<div className="bg-grid"/>
|
||||
<div className="particles">
|
||||
{particles.map(p => (
|
||||
<span key={p.id} className="particle" style={{
|
||||
left: `${p.left}%`,
|
||||
bottom: "-10px",
|
||||
width: p.size,
|
||||
height: p.size,
|
||||
animationDuration: `${p.dur}s`,
|
||||
animationDelay: `${p.delay}s`,
|
||||
opacity: p.opacity,
|
||||
}}/>
|
||||
))}
|
||||
</div>
|
||||
<div className="cursor-glow" ref={glowRef}/>
|
||||
<div className="scroll-progress" ref={progressRef}/>
|
||||
|
||||
<Nav/>
|
||||
<Hero/>
|
||||
<TrustMarquee/>
|
||||
<Manifesto/>
|
||||
<LiveMetrics/>
|
||||
<SpecSheet/>
|
||||
<CoreFeatures/>
|
||||
<ConnectedOps/>
|
||||
<MobileSection/>
|
||||
<Analytics/>
|
||||
<FieldIntelligence/>
|
||||
<Testimonials/>
|
||||
<Pricing/>
|
||||
<FinalCTA/>
|
||||
{/* <Footer/> hidden — links were all pointing to Book Demo; revisit later */}
|
||||
|
||||
{/* Sticky system pill */}
|
||||
<div className="sys-pill">
|
||||
<span className="dot"/>
|
||||
<span>SYS · ALL GREEN</span>
|
||||
<span style={{ color: "var(--cyan-2)" }}>| 142 MS</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
|
||||
@@ -0,0 +1,649 @@
|
||||
/* global React, Reveal, GlowCard, Icon, Sparkline, DonutChart, BlueprintHouse, Counter */
|
||||
// Features, Connected Operations, Mobile
|
||||
|
||||
const FEATURES = [
|
||||
{ code: "M.01", name: "AI Copilot", icon: "ai", color: "var(--cyan)",
|
||||
desc: "An intelligent operator that thinks ahead, rerouting crews, flagging risk, and surfacing next best actions in real time.",
|
||||
tag: "ALWAYS ON" },
|
||||
{ code: "M.02", name: "Smart Scheduling", icon: "calendar", color: "var(--orange)",
|
||||
desc: "Auto-organizes routes, weather, conflicts and dependencies. Suggests the perfect time, every time.",
|
||||
tag: "ADAPTIVE" },
|
||||
{ code: "M.03", name: "Crew Automation", icon: "crew", color: "var(--green)",
|
||||
desc: "Dispatch, check-ins, and timecards run themselves. Field teams stay aligned without the chatter.",
|
||||
tag: "HANDS-FREE" },
|
||||
{ code: "M.04", name: "GPS Fleet Tracking", icon: "fleet", color: "var(--violet)",
|
||||
desc: "Live vehicle telemetry, geofenced job zones, ETA forecasting, and route optimization at a glance.",
|
||||
tag: "REAL-TIME" },
|
||||
{ code: "M.05", name: "Budget Intelligence", icon: "budget", color: "var(--cyan)",
|
||||
desc: "Predictive margins, automated cost capture, and instant variance alerts before they bite.",
|
||||
tag: "PREDICTIVE" },
|
||||
{ code: "M.06", name: "Real-time Reporting", icon: "report", color: "var(--orange)",
|
||||
desc: "Production, profit, and performance, generated continuously from connected operations data.",
|
||||
tag: "LIVE" },
|
||||
{ code: "M.07", name: "Client Communication", icon: "chat", color: "var(--green)",
|
||||
desc: "Branded portal, automated touchpoints, and a single thread per project. Nothing falls through.",
|
||||
tag: "BRANDED" },
|
||||
{ code: "M.08", name: "Field Intelligence", icon: "field", color: "var(--violet)",
|
||||
desc: "AI-powered measurements, photo evidence, claim-ready reports and on-site decisions in minutes.",
|
||||
tag: "AI VISION" },
|
||||
];
|
||||
|
||||
// ============ SPEC SHEET - six-system architecture =============
|
||||
function SpecSheet() {
|
||||
const systems = [
|
||||
{ code: "S.01", name: "CRM", icon: "lead", color: "var(--cyan)", desc: "Pipeline, leads & relationships" },
|
||||
{ code: "S.02", name: "Canvas", icon: "doc", color: "var(--orange)", desc: "Business development" },
|
||||
{ code: "S.03", name: "Measurements",icon: "target", color: "var(--green)", desc: "AI roof & site dimensions" },
|
||||
{ code: "S.04", name: "Client", icon: "chat", color: "var(--violet)", desc: "Branded portals & comms" },
|
||||
{ code: "S.05", name: "Billing", icon: "money", color: "var(--cyan)", desc: "Invoicing, payments, AR" },
|
||||
{ code: "S.06", name: "Estimating", icon: "report", color: "var(--orange)", desc: "Proposals that close" },
|
||||
];
|
||||
return (
|
||||
<section className="section tight" id="systems">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 48, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§04</span>
|
||||
<span>SPEC SHEET · CORE STACK</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22 }}>
|
||||
Six systems.<br/>
|
||||
<span className="accent">One intelligent CRM/ERP.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="row gap-12">
|
||||
<span className="dim-tag">// ALL CONNECTED</span>
|
||||
<span className="dim-tag">// ALL AUTOMATED</span>
|
||||
<span className="dim-tag" style={{ color: "var(--orange-2)", borderColor: "rgba(255,138,30,0.4)" }}>// BUILT TO PERFORM</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={1}>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
padding: "44px 36px 32px",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 20,
|
||||
background: `
|
||||
repeating-linear-gradient(0deg, transparent 0 26px, rgba(0,209,255,0.04) 26px 27px),
|
||||
radial-gradient(700px 280px at 50% 0%, rgba(0,209,255,0.12), transparent 70%),
|
||||
rgba(2,6,14,0.55)
|
||||
`,
|
||||
}}>
|
||||
<DimCorner pos="tl"/>
|
||||
<DimCorner pos="tr"/>
|
||||
<DimCorner pos="bl"/>
|
||||
<DimCorner pos="br"/>
|
||||
|
||||
{/* Spec header bar */}
|
||||
<div className="row" style={{
|
||||
justifyContent: "space-between",
|
||||
paddingBottom: 18,
|
||||
borderBottom: "1px dashed var(--border)",
|
||||
marginBottom: 28,
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.2em",
|
||||
color: "var(--muted)",
|
||||
}}>
|
||||
<span>LYNKEDUP PRO · TECHNICAL SPECIFICATION</span>
|
||||
<span style={{ color: "var(--cyan-2)" }}>SHEET 04 / 10</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 18 }}>
|
||||
{systems.map((s, i) => (
|
||||
<Reveal key={s.code} delay={(i % 6) + 1}>
|
||||
<div style={{
|
||||
padding: 20,
|
||||
border: `1px solid ${s.color}55`,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(180deg, ${s.color}10, transparent)`,
|
||||
position: "relative",
|
||||
transition: "transform 0.3s, box-shadow 0.3s",
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.transform = "translateY(-4px)";
|
||||
e.currentTarget.style.boxShadow = `0 18px 40px -16px ${s.color}99`;
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.transform = "none";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
border: `1px solid ${s.color}99`,
|
||||
background: `${s.color}18`,
|
||||
display: "grid", placeItems: "center",
|
||||
marginBottom: 18,
|
||||
boxShadow: `inset 0 0 18px -4px ${s.color}77`,
|
||||
}}>
|
||||
<Icon name={s.icon} size={22} stroke={s.color}/>
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 10, color: s.color, letterSpacing: "0.18em", marginBottom: 6 }}>{s.code} /</div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontSize: 18, fontWeight: 600, marginBottom: 6 }}>{s.name}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)", lineHeight: 1.45 }}>{s.desc}</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* spec footer */}
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
gap: 0,
|
||||
marginTop: 32,
|
||||
borderTop: "1px dashed var(--border)",
|
||||
paddingTop: 22,
|
||||
}}>
|
||||
{[
|
||||
{ l: "SEAMLESS COLLABORATION", d: "Office, sales reps, crews & subs aligned", c: "var(--cyan)" },
|
||||
{ l: "SMARTER DECISIONS", d: "AI insights you can act on", c: "var(--orange)" },
|
||||
{ l: "PROFITABLE GROWTH", d: "Close more, margin up", c: "var(--green)" },
|
||||
{ l: "ZERO HANDOFF LOSS", d: "Same data layer end-to-end", c: "var(--violet)" },
|
||||
].map(f => (
|
||||
<div key={f.l} style={{ paddingRight: 18, borderRight: "1px solid var(--border)" }}>
|
||||
<div className="mono" style={{ fontSize: 10, color: f.c, letterSpacing: "0.16em", marginBottom: 8 }}>● {f.l}</div>
|
||||
<div style={{ fontSize: 13, color: "var(--ink-2)" }}>{f.d}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CoreFeatures() {
|
||||
return (
|
||||
<section className="section" id="features">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 56, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§05</span>
|
||||
<span>PLATFORM CAPABILITIES</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22, maxWidth: 700 }}>
|
||||
Eight modules.<br/>
|
||||
<span className="accent">One operating brain.</span>
|
||||
</h2>
|
||||
<p className="sub" style={{ marginTop: 18 }}>
|
||||
Every module shares the same connected data layer,
|
||||
so insight in one corner of the business becomes action everywhere else.
|
||||
</p>
|
||||
</div>
|
||||
<span className="dim-tag" style={{ height: 30, padding: "4px 12px" }}>// MODULES 01-08</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 20 }}>
|
||||
{FEATURES.map((f, i) => (
|
||||
<Reveal key={f.name} delay={(i % 4) + 1}>
|
||||
<GlowCard className="feature-card" style={{ minHeight: 280, padding: 0, display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ padding: 26, flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 22 }}>
|
||||
<div style={{
|
||||
width: 54, height: 54, borderRadius: 14,
|
||||
border: `1px solid ${f.color}55`,
|
||||
background: `linear-gradient(180deg, ${f.color}22, transparent)`,
|
||||
display: "grid", placeItems: "center",
|
||||
boxShadow: `0 0 24px -6px ${f.color}88, inset 0 0 24px -6px ${f.color}55`,
|
||||
}}>
|
||||
<Icon name={f.icon} size={26} stroke={f.color}/>
|
||||
</div>
|
||||
<span className="mono" style={{ fontSize: 10, color: f.color, letterSpacing: "0.2em" }}>{f.code} /</span>
|
||||
</div>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 10 }}>
|
||||
<h3 className="h-card">{f.name}</h3>
|
||||
<span className="mono" style={{ fontSize: 9, color: f.color, letterSpacing: "0.18em", opacity: 0.8 }}>{f.tag}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.5, margin: 0 }}>{f.desc}</p>
|
||||
<div style={{ flex: 1 }}/>
|
||||
</div>
|
||||
<div style={{
|
||||
height: 2,
|
||||
background: `linear-gradient(90deg, transparent, ${f.color}, transparent)`,
|
||||
opacity: 0.6,
|
||||
}}/>
|
||||
</GlowCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- Connected Operations -----------------
|
||||
function ConnectedOps() {
|
||||
// workflow nodes positioned around central blueprint
|
||||
const nodes = [
|
||||
{ x: 12, y: 14, label: "Leads", icon: "lead", sub: "Auto-qualified" },
|
||||
{ x: 12, y: 38, label: "Schedule", icon: "calendar", sub: "Adapts in real time" },
|
||||
{ x: 12, y: 62, label: "Inspections",icon: "doc", sub: "Field-captured" },
|
||||
{ x: 88, y: 14, label: "Estimates", icon: "money", sub: "Create faster" },
|
||||
{ x: 88, y: 38, label: "Jobs", icon: "hammer", sub: "Every step tracked" },
|
||||
{ x: 88, y: 62, label: "Invoices", icon: "report", sub: "Paid faster" },
|
||||
];
|
||||
return (
|
||||
<section className="section" id="platform">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 56, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§06</span>
|
||||
<span>CONNECTED OPERATIONS</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22 }}>
|
||||
All your work.<br/>
|
||||
Connected. <span className="accent">All in one place.</span>
|
||||
</h2>
|
||||
<p className="sub" style={{ marginTop: 18 }}>
|
||||
From leads to final invoice, every operational detail flows
|
||||
through the same nervous system. Nothing slips through the cracks.
|
||||
</p>
|
||||
</div>
|
||||
<span className="dim-tag" style={{ height: 30, padding: "4px 12px" }}>// ONE SOURCE OF TRUTH</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={1}>
|
||||
<GlowCard style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
height: 720,
|
||||
background: `
|
||||
radial-gradient(800px 400px at 50% 50%, rgba(0,209,255,0.12), transparent 70%),
|
||||
linear-gradient(180deg, rgba(4,10,22,0.4), rgba(2,6,14,0.6))
|
||||
`,
|
||||
}}>
|
||||
{/* SVG connection lines */}
|
||||
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
|
||||
<defs>
|
||||
<linearGradient id="line-g" x1="0" x2="1">
|
||||
<stop offset="0" stopColor="#00d1ff" stopOpacity="0"/>
|
||||
<stop offset="0.5" stopColor="#00d1ff" stopOpacity="0.7"/>
|
||||
<stop offset="1" stopColor="#00d1ff" stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{nodes.map((n, i) => {
|
||||
const cx = 50, cy = 50;
|
||||
return (
|
||||
<g key={i}>
|
||||
<path
|
||||
d={`M${n.x} ${n.y} C ${(n.x+cx)/2} ${n.y}, ${(n.x+cx)/2} ${cy}, ${cx} ${cy}`}
|
||||
fill="none"
|
||||
stroke="rgba(0,209,255,0.32)"
|
||||
strokeWidth="0.15"
|
||||
strokeDasharray="0.6 0.6"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
<circle r="0.5" fill="#7be8ff">
|
||||
<animateMotion
|
||||
dur={`${3 + i * 0.3}s`}
|
||||
repeatCount="indefinite"
|
||||
path={`M${n.x} ${n.y} C ${(n.x+cx)/2} ${n.y}, ${(n.x+cx)/2} ${cy}, ${cx} ${cy}`}
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Central wireframe house */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
<BlueprintHouse size={520} animate={false} withScan={true}/>
|
||||
</div>
|
||||
|
||||
{/* Central hex logo overlay */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 110, height: 110,
|
||||
display: "grid", placeItems: "center",
|
||||
background: "radial-gradient(circle, rgba(0,209,255,0.3), transparent 60%)",
|
||||
zIndex: 2,
|
||||
}}>
|
||||
<img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro"
|
||||
width="100" height="100"
|
||||
style={{ width: 100, height: 100, objectFit: "contain", filter: "drop-shadow(0 0 20px rgba(0,209,255,0.9))" }}/>
|
||||
</div>
|
||||
|
||||
{/* Workflow nodes */}
|
||||
{nodes.map((n, i) => (
|
||||
<div key={n.label} style={{
|
||||
position: "absolute",
|
||||
left: `${n.x}%`, top: `${n.y}%`,
|
||||
transform: `translate(${n.x < 50 ? "-10%" : "-90%"}, -50%)`,
|
||||
zIndex: 3,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 200,
|
||||
padding: 14,
|
||||
background: "linear-gradient(180deg, rgba(8,18,36,0.92), rgba(4,10,22,0.88))",
|
||||
border: "1px solid rgba(0,209,255,0.35)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(14px)",
|
||||
boxShadow: "0 8px 24px -10px rgba(0,209,255,0.45)",
|
||||
animation: `floatY ${5 + i * 0.4}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.3}s`,
|
||||
}}>
|
||||
<div className="row gap-12" style={{ marginBottom: 6 }}>
|
||||
<Icon name={n.icon} size={18} stroke="var(--cyan)"/>
|
||||
<span style={{ fontFamily: "var(--font-display)", fontSize: 15, fontWeight: 600 }}>{n.label}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>{n.sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Footer ribbon */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: 24, right: 24, bottom: 22,
|
||||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||
padding: "14px 22px",
|
||||
background: "rgba(0,209,255,0.06)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}>
|
||||
<div className="row gap-16">
|
||||
<span className="live-dot"/>
|
||||
<span className="mono" style={{ fontSize: 12, color: "var(--cyan-2)", letterSpacing: "0.18em" }}>
|
||||
ONE SYSTEM · ONE SOURCE OF TRUTH
|
||||
</span>
|
||||
</div>
|
||||
<div className="row gap-24" style={{ fontSize: 11, color: "var(--muted)" }} >
|
||||
<span><Counter to={2847} /> events / hr</span>
|
||||
<span>·</span>
|
||||
<span><Counter to={147} suffix=" ms"/> sync</span>
|
||||
<span>·</span>
|
||||
<span className="mono" style={{ color: "var(--green)" }}>● HEALTHY</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlowCard>
|
||||
</Reveal>
|
||||
|
||||
{/* Outcome bar */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 20, marginTop: 32 }}>
|
||||
{[
|
||||
{ icon: "clock", color: "var(--cyan)", title: "Save time", desc: "Automate the busy work and focus on what matters." },
|
||||
{ icon: "target", color: "var(--orange)", title: "Reduce errors", desc: "Connected data means fewer mistakes." },
|
||||
{ icon: "growth", color: "var(--green)", title: "Grow faster", desc: "Better systems. Better decisions. Bigger results." },
|
||||
].map((o, i) => (
|
||||
<Reveal key={o.title} delay={i + 1}>
|
||||
<div style={{
|
||||
padding: 22, display: "flex", gap: 16, alignItems: "flex-start",
|
||||
border: "1px solid var(--border)", borderRadius: 16,
|
||||
background: "rgba(0,209,255,0.03)",
|
||||
}}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
border: `1px solid ${o.color}55`,
|
||||
display: "grid", placeItems: "center",
|
||||
background: `${o.color}15`,
|
||||
}}>
|
||||
<Icon name={o.icon} size={22} stroke={o.color}/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontSize: 17, fontWeight: 600, marginBottom: 4 }}>{o.title}</div>
|
||||
<div style={{ fontSize: 13, color: "var(--muted)" }}>{o.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- Mobile Experience ----------------
|
||||
function MobilePhone() {
|
||||
return (
|
||||
<div style={{ position: "relative", width: 320, height: 660 }}>
|
||||
{/* Phone frame */}
|
||||
<div style={{
|
||||
width: "100%", height: "100%",
|
||||
background: "linear-gradient(160deg, #1a2540, #07101e)",
|
||||
borderRadius: 42,
|
||||
padding: 8,
|
||||
boxShadow: "0 60px 80px -30px rgba(0,209,255,0.45), 0 0 0 1px rgba(0,209,255,0.25), inset 0 0 0 1px rgba(255,255,255,0.05)",
|
||||
position: "relative",
|
||||
}}>
|
||||
<div style={{
|
||||
width: "100%", height: "100%",
|
||||
background: "linear-gradient(180deg, #050c18, #02060e)",
|
||||
borderRadius: 34,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}>
|
||||
{/* Notch */}
|
||||
<div style={{
|
||||
position: "absolute", top: 12, left: "50%", transform: "translateX(-50%)",
|
||||
width: 110, height: 26, background: "#000", borderRadius: 13, zIndex: 5,
|
||||
}}/>
|
||||
{/* Status bar */}
|
||||
<div style={{
|
||||
display: "flex", justifyContent: "space-between",
|
||||
padding: "16px 24px 0",
|
||||
fontSize: 12, fontFamily: "var(--font-display)", fontWeight: 600,
|
||||
color: "#fff",
|
||||
}}>
|
||||
<span>9:41</span>
|
||||
<span style={{ opacity: 0 }}>.</span>
|
||||
<span className="mono">●●●● <span style={{ marginLeft: 4 }}>5G</span></span>
|
||||
</div>
|
||||
|
||||
{/* App content */}
|
||||
<div style={{ padding: "40px 18px 18px", display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 6 }}>
|
||||
<span style={{ fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 16 }}>June 23, 2026</span>
|
||||
<Icon name="bell" size={18} stroke="var(--cyan)"/>
|
||||
</div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--cyan-2)", letterSpacing: "0.16em" }}>TODAY · 3 SCHEDULED</div>
|
||||
|
||||
{/* Schedule cards */}
|
||||
{[
|
||||
{ t: "10:00 AM", title: "Roof Inspection", sub: "123 Maple St", active: true },
|
||||
{ t: "1:30 PM", title: "Estimate Review", sub: "456 Oak Ave" },
|
||||
{ t: "11:00 AM", title: "Material Order", sub: "789 Pine Rd" },
|
||||
].map((s, i) => (
|
||||
<div key={i} style={{
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
background: s.active ? "linear-gradient(180deg, #ff8a1e, #d96100)" : "rgba(0,209,255,0.07)",
|
||||
border: s.active ? "1px solid #ffb24d" : "1px solid var(--border)",
|
||||
color: s.active ? "#1a0800" : "var(--ink)",
|
||||
boxShadow: s.active ? "0 12px 24px -8px rgba(255,138,30,0.6)" : "none",
|
||||
}}>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 13 }}>{s.t}</div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, marginTop: 2 }}>{s.title}</div>
|
||||
<div style={{ fontSize: 11, opacity: 0.75 }}>{s.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Bottom tabs */}
|
||||
<div className="row" style={{ justifyContent: "space-around", marginTop: 16, padding: "10px 0", borderTop: "1px solid rgba(0,209,255,0.15)" }}>
|
||||
{["calendar","sync","bell","pin"].map((ic, i) => (
|
||||
<div key={i} style={{ width: 32, height: 32, borderRadius: 8, display: "grid", placeItems: "center",
|
||||
background: i === 0 ? "rgba(0,209,255,0.18)" : "transparent" }}>
|
||||
<Icon name={ic} size={16} stroke={i === 0 ? "var(--cyan)" : "var(--muted)"}/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side notification glows */}
|
||||
<div style={{ position: "absolute", top: 80, right: -180, animation: "floatY 5s ease-in-out infinite" }}>
|
||||
<div style={{
|
||||
width: 220, padding: 14,
|
||||
background: "rgba(0,209,255,0.08)",
|
||||
border: "1px solid rgba(0,209,255,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(14px)",
|
||||
boxShadow: "0 12px 30px -10px rgba(0,209,255,0.5)",
|
||||
}}>
|
||||
<div className="row gap-8" style={{ marginBottom: 6 }}>
|
||||
<span className="live-dot"/>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--cyan-2)", letterSpacing: "0.16em" }}>NEW ALERT</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>Diego's crew clocked in</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>456 Oak Ave · 3 min ago</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: "absolute", bottom: 80, left: -200, animation: "floatY 6s ease-in-out infinite", animationDelay: "1s" }}>
|
||||
<div style={{
|
||||
width: 240, padding: 14,
|
||||
background: "rgba(255,138,30,0.07)",
|
||||
border: "1px solid rgba(255,138,30,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(14px)",
|
||||
boxShadow: "0 12px 30px -10px rgba(255,138,30,0.45)",
|
||||
}}>
|
||||
<div className="row gap-8" style={{ marginBottom: 6 }}>
|
||||
<Icon name="spark" size={12} stroke="var(--orange)"/>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--orange-2)", letterSpacing: "0.16em" }}>AI REMINDER</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>Leave by 9:42 AM</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>Traffic on Maple St, moderate</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: "absolute", top: 350, right: -160, animation: "floatY 5.5s ease-in-out infinite", animationDelay: "2s" }}>
|
||||
<div style={{
|
||||
width: 180, padding: 12,
|
||||
background: "rgba(43,212,156,0.07)",
|
||||
border: "1px solid rgba(43,212,156,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(14px)",
|
||||
boxShadow: "0 12px 30px -10px rgba(43,212,156,0.4)",
|
||||
}}>
|
||||
<div className="row gap-8" style={{ marginBottom: 4 }}>
|
||||
<Icon name="report" size={12} stroke="var(--green)"/>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--green)", letterSpacing: "0.16em" }}>DAILY REPORT</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>3 jobs · $12.4K</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>Generated · 4:55 PM</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ambient glow behind phone */}
|
||||
<div className="ring-glow" style={{
|
||||
width: 500, height: 500, top: 50, left: -100,
|
||||
background: "radial-gradient(circle, rgba(0,209,255,0.4), transparent 60%)", zIndex: -1,
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSection() {
|
||||
return (
|
||||
<section className="section" id="mobile">
|
||||
<div className="beam top"/>
|
||||
<div className="shell split">
|
||||
<div>
|
||||
<Reveal>
|
||||
<span className="section-number">
|
||||
<span className="num">§07</span>
|
||||
<span>FIELD MOBILE</span>
|
||||
</span>
|
||||
</Reveal>
|
||||
<Reveal delay={1}>
|
||||
<h2 className="h-section" style={{ marginTop: 22 }}>
|
||||
Never miss what matters.<br/>
|
||||
<span className="accent">Stay ahead every day.</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal delay={2}>
|
||||
<p className="sub" style={{ marginTop: 24 }}>
|
||||
The LynkedUp Pro mobile companion turns your phone into a field
|
||||
command center. Smart scheduling, real-time alerts, and AI reminders
|
||||
keep your day on track, and your business moving forward.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={3}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14, marginTop: 36 }}>
|
||||
{[
|
||||
{ icon: "calendar", title: "Smart scheduling", sub: "Automatically organize and adapt.", color: "var(--cyan)" },
|
||||
{ icon: "bell", title: "Real-time alerts", sub: "Know what's next. Always.", color: "var(--orange)" },
|
||||
{ icon: "pin", title: "Location ready", sub: "Get directions and job details in one tap.", color: "var(--green)" },
|
||||
{ icon: "sync", title: "Sync everywhere", sub: "Stay aligned across your entire team.", color: "var(--violet)" },
|
||||
].map(m => (
|
||||
<div key={m.title} className="row gap-16" style={{
|
||||
padding: "14px 16px",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 12,
|
||||
background: "rgba(0,209,255,0.03)",
|
||||
transition: "border-color 0.25s, transform 0.25s",
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = "var(--border-strong)";
|
||||
e.currentTarget.style.transform = "translateX(6px)";
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = "var(--border)";
|
||||
e.currentTarget.style.transform = "none";
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 42, height: 42, borderRadius: 10,
|
||||
border: `1px solid ${m.color}55`,
|
||||
background: `${m.color}15`,
|
||||
display: "grid", placeItems: "center",
|
||||
}}>
|
||||
<Icon name={m.icon} size={20} stroke={m.color}/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 15, letterSpacing: "0.04em", textTransform: "uppercase" }}>{m.title}</div>
|
||||
<div style={{ fontSize: 13, color: "var(--muted)" }}>{m.sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<Reveal delay={2}>
|
||||
<img
|
||||
src="page/assets/mobile.png"
|
||||
alt="LynkedUp Pro mobile app"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 320,
|
||||
display: "block",
|
||||
borderRadius: 28,
|
||||
border: "1px solid var(--border-strong)",
|
||||
boxShadow: "0 40px 90px -30px rgba(0,0,0,0.85), 0 0 0 1px rgba(0,209,255,0.08)",
|
||||
}}
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { CoreFeatures, ConnectedOps, MobileSection, SpecSheet });
|
||||
@@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>LynkedUp Pro - AI Construction Operating System</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="styles.css"/>
|
||||
|
||||
<style>
|
||||
/* Local animation primitives the components rely on */
|
||||
@keyframes floatY {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
@keyframes growBar {
|
||||
from { transform: scaleY(0); }
|
||||
to { transform: scaleY(1); }
|
||||
}
|
||||
@keyframes growW {
|
||||
from { width: 0; }
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- React 18 + Babel -->
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Components in dependency order -->
|
||||
<script type="text/babel" src="ui.jsx"></script>
|
||||
<script type="text/babel" src="hero.jsx"></script>
|
||||
<script type="text/babel" src="features.jsx"></script>
|
||||
<script type="text/babel" src="sections.jsx"></script>
|
||||
<script type="text/babel" src="app.jsx"></script>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,543 @@
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>LynkedUp Pro - Know Exactly Where Every Job Stands, Right Now</title>
|
||||
<meta name="description" content="Projects module: full stage pipeline, estimate statuses, live budget breakdown (team cost, commission, material). Zero guesswork on every job."/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:stretch;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.shot{display:block;width:100%;height:100%;border-radius:18px;border:1px solid var(--border);object-fit:cover;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
/* ── background image helpers ── */
|
||||
.has-bg{position:relative;overflow:hidden}
|
||||
.has-bg>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.has-bg>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.has-bg>.container,.has-bg>.hero-bg,.has-bg>.eyebrow,.has-bg>h1,.has-bg>.hero-sub,.has-bg>.hero-actions,.has-bg>.micro-row,.has-bg>.stat-row{position:relative;z-index:2}
|
||||
/* ── bigger, more impactful headings ── */
|
||||
.s-title{font-size:clamp(36px,5.2vw,64px);line-height:.96}
|
||||
/* ── CTA strip ── */
|
||||
.cta-strip{padding:86px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.cta-strip>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.cta-strip>.glow{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:720px;height:380px;border-radius:50%;z-index:1;pointer-events:none}
|
||||
.cta-strip>.wrap{position:relative;z-index:2;max-width:700px;margin:0 auto}
|
||||
.strip-tag{display:inline-flex;align-items:center;gap:7px;padding:5px 16px;border-radius:100px;margin-bottom:22px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.strip-tdot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:blink 2s ease infinite}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5.2vw,62px);font-weight:900;line-height:.96;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.75;max-width:520px;margin:0 auto 30px}
|
||||
.strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.strip-trust{display:flex;gap:18px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.strip-trust b{color:var(--green)}
|
||||
/* ── metric compare cards ── */
|
||||
.metric-pills{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:30px}
|
||||
.mpill{background:var(--card);border-radius:12px;padding:16px 22px;text-align:center;min-width:130px}
|
||||
.mpill-val{font-family:'Barlow Condensed',sans-serif;font-size:30px;font-weight:900;line-height:1}
|
||||
.mpill-lab{font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
|
||||
@media(max-width:1280px){section{padding:80px 24px}.cta-section{padding:80px 24px}.cta-strip{padding:76px 24px}}
|
||||
@media(max-width:1024px){nav{padding:14px 24px}section{padding:72px 22px}.cta-section{padding:72px 22px}.cta-strip{padding:68px 22px}.pain-grid{grid-template-columns:repeat(2,1fr)}.sol-split{gap:36px}h1{font-size:clamp(44px,7vw,76px)}.hero{padding:108px 20px 64px}.proof-card{padding:30px 26px}.proof-result{top:18px;right:18px}.stat-row{gap:28px;margin-top:48px}}
|
||||
@media(max-width:768px){nav{padding:12px 18px}section{padding:56px 18px}.cta-section{padding:56px 18px}.cta-strip{padding:56px 18px}.hero{min-height:auto;padding:96px 16px 56px}h1{font-size:clamp(40px,9vw,64px);line-height:.95}.hero-sub{font-size:15px}.s-title{font-size:clamp(30px,7vw,46px)}.stat-row{gap:18px;margin-top:40px}.stat+.stat::before{display:none}.stat-num{font-size:32px}.proof-card{padding:26px 22px;margin-top:32px}.proof-result{position:static;display:inline-block;margin-bottom:14px}.proof-quote{font-size:15px}.cta-section h2{font-size:clamp(30px,7vw,46px)}.cta-strip h2{font-size:clamp(28px,7vw,46px)}.sol-points li{font-size:14px}.p-item{padding:22px 18px}.metric-pills{gap:8px}.mpill{min-width:110px;padding:14px 16px}.mpill-val{font-size:24px}}
|
||||
@media(max-width:480px){nav{padding:10px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}section{padding:48px 16px}.cta-section{padding:48px 16px}.cta-strip{padding:48px 16px}.hero{padding:88px 14px 48px}h1{font-size:clamp(34px,10vw,52px);letter-spacing:-.5px}.hero-sub{font-size:14px;margin-top:18px}.s-title{font-size:clamp(28px,8vw,40px);letter-spacing:0}.s-sub{font-size:15px}.micro{font-size:12px}.micro-row{gap:10px}.stat-row{flex-direction:column;gap:18px;margin-top:32px}.stat-num{font-size:28px}.pain-grid{margin-top:30px}.p-item h3{font-size:15px}.p-item p{font-size:13px}.proof-card{padding:22px 18px;border-radius:14px}.proof-quote{font-size:14px;line-height:1.7}.proof-author{flex-wrap:wrap;gap:10px}.cta-section h2,.cta-strip h2{font-size:clamp(26px,8vw,38px)}.cta-section p,.cta-strip p{font-size:14px}.btn-primary,.btn-ghost{padding:12px 22px;font-size:15px}.sol-stats{grid-template-columns:1fr 1fr;gap:8px}.sol-stat{padding:12px}.sol-stat-num{font-size:22px}.mock-card{border-radius:14px}.metric-pills{flex-wrap:wrap}.mpill{min-width:90px;padding:12px 14px}.mpill-val{font-size:22px}.cta-micro{gap:8px;font-size:11px}.strip-tag{font-size:10px;padding:4px 12px;letter-spacing:.8px}footer{padding:28px 16px 22px}footer p{font-size:12px}.mob-sticky{padding:10px 12px}.mob-sticky a{font-size:13px;padding:11px}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}}
|
||||
@media(max-width:360px){h1{font-size:32px}.s-title{font-size:26px}.cta-section h2,.cta-strip h2{font-size:24px}.btn-primary,.btn-ghost{padding:11px 18px;font-size:14px}.sol-stats{grid-template-columns:1fr}}
|
||||
@media(max-width:860px){.mob-sticky{display:flex}body{padding-bottom:72px}.sol-split{grid-template-columns:1fr!important}.pain-grid{grid-template-columns:1fr!important}.shot{height:auto}}
|
||||
@media(max-width:768px){.stat+.stat::before{display:none}.stat-row{gap:18px}}
|
||||
@media(max-width:560px){.sol-stats{grid-template-columns:1fr 1fr}.stat-row{flex-direction:column;gap:18px}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin:24px auto 0}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}}
|
||||
@media(max-width:360px){.sol-stats{grid-template-columns:1fr}}
|
||||
@keyframes hpulse{0%,100%{opacity:.9;transform:scale(1)}50%{opacity:.45;transform:scale(1.3)}}
|
||||
.stage-pill{display:inline-flex;align-items:center;gap:6px;padding:5px 14px;border-radius:100px;font-size:12px;font-weight:600;font-family:'Outfit',sans-serif}
|
||||
</style></head><body>
|
||||
<nav><a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a><a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a></nav>
|
||||
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1800&q=80');filter:brightness(.11) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(5,8,15,.5) 0%,rgba(5,8,15,.8) 55%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(139,92,246,.1),transparent 65%)"></div></div>
|
||||
<div class="eyebrow"><span class="dot" style="background:#a855f7"></span> Projects · Stages · Estimates · Budget</div>
|
||||
<h1>Right Now, One Job<br>Is Over Budget.<br><span style="color:#a855f7">Do You Know Which?</span></h1>
|
||||
<p class="hero-sub"><strong>Which jobs are stalled at estimate? Which ones are bleeding margin?</strong> Without a live projects view, you find out too late, after the job closes and the money's already gone. LynkedUp Pro is your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">See Projects Module →</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row" style="margin-top:18px">
|
||||
<span class="micro" style="color:var(--orange)">4.9★ · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>✅</span> Full stage pipeline</span>
|
||||
<span class="micro"><span>✅</span> Live budget per job</span>
|
||||
<span class="micro"><span>✅</span> Estimate status tracking</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:#a855f7">6</div><div class="stat-label">Pipeline stages tracked</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">4</div><div class="stat-label">Estimate statuses (live)</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">Live</div><div class="stat-label">Budget vs actual per job</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">1-click</div><div class="stat-label">PDF estimate download</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=1800&q=80');filter:brightness(.1) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(11,18,32,.98) 0%,rgba(11,18,32,.92) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">The Margin Was Gone<br>Before the Job Was <span style="color:var(--red)">Even Finished.</span></h2>
|
||||
<p class="s-sub">Most roofing software shows you a pipeline. LynkedUp Pro shows you the full financial picture of every job in real time, so problems surface before they cost you.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item"><div class="p-icon">🗂️</div><h3>Estimates Stalling Invisibly</h3><p>A rep sent a draft 5 days ago. Has the homeowner seen it? Approved it? Rejected it? Without status tracking you find out when it's too late.</p><span class="p-cost">Cost: Jobs lost to follow-up gaps</span></div>
|
||||
<div class="p-item"><div class="p-icon">💸</div><h3>Jobs Going Over Budget Silently</h3><p>Team cost, commissions, and material spend accumulate with no warning. By the time you notice the margin is gone, the job is already complete.</p><span class="p-cost">Cost: Margin erosion per job</span></div>
|
||||
<div class="p-item"><div class="p-icon">📋</div><h3>Stage Confusion Across the Team</h3><p>Your sales rep thinks the job is at "appointment scheduled." Your manager thinks it's at "estimate sent." Your admin thinks it's still a lead. No one knows what's actually happening.</p><span class="p-cost">Cost: Coordination overhead daily</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══ CTA STRIP 1 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(10,5,18,.97) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(168,85,247,.08),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(168,85,247,.1);border:1px solid rgba(168,85,247,.25);color:#a855f7">
|
||||
<span class="strip-tdot" style="background:#a855f7"></span>
|
||||
Every day without live tracking is a day you're flying blind
|
||||
</div>
|
||||
<h2>Your Jobs Are Running.<br><span style="color:#a855f7">Are You Running Them</span><br>or Are They <span style="color:var(--red)">Running You?</span></h2>
|
||||
<p>Right now, somewhere in your active jobs, a material order just pushed a budget over the edge. An estimate has been sitting unread for 4 days. A stage hasn't moved in a week. You don't know - but you should.</p>
|
||||
<!-- 3 pain pills -->
|
||||
<div class="metric-pills">
|
||||
<div class="mpill" style="border:1px solid rgba(239,68,68,.2)">
|
||||
<div class="mpill-val" style="color:var(--red)">3-5</div>
|
||||
<div class="mpill-lab">Jobs over budget unnoticed / month</div>
|
||||
</div>
|
||||
<div class="mpill" style="border:1px solid rgba(245,158,11,.2)">
|
||||
<div class="mpill-val" style="color:var(--orange)">4 days</div>
|
||||
<div class="mpill-lab">Avg estimate sitting unread</div>
|
||||
</div>
|
||||
<div class="mpill" style="border:1px solid rgba(168,85,247,.2)">
|
||||
<div class="mpill-val" style="color:#a855f7">$0</div>
|
||||
<div class="mpill-lab">Margin warning without LynkedUp</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Show Me Every Job Live →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> Free 30-min demo</span>
|
||||
<span><b>✅</b> No credit card</span>
|
||||
<span><b>✅</b> Live in 24 hrs</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1800&q=80');filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(90deg,rgba(5,8,15,1) 44%,rgba(5,8,15,.72) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:#a855f7">The Fix</div>
|
||||
<h2 class="s-title">Catch Overruns <span style="color:#a855f7">Live.</span><br>Close Estimates <span style="color:var(--green)">Faster.</span><br>Know Every Stage.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro's Projects module tracks every job through a defined stage pipeline with live estimate status and a real-time budget breakdown, so you always know exactly where you stand.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">🔄</span><span><strong>6-stage pipeline</strong> - Contacted → Appointment Scheduled → Estimate Sent → Approved → Job Active → Invoiced. Every stage visible to every stakeholder.</span></li>
|
||||
<li><span class="ico">📄</span><span><strong>4 estimate statuses</strong> - Draft, Sent, Approved, Rejected. One-click PDF download. Customer signs digitally on the spot.</span></li>
|
||||
<li><span class="ico">💰</span><span><strong>Live budget per job</strong> - team cost, commission, actor cost, material cost all tracked in real time against project budget. Margin warning before it's gone.</span></li>
|
||||
<li><span class="ico">👤</span><span><strong>Role-based visibility</strong> - reps see their jobs, managers see their team, owners see everything. Right data to the right person.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:#a855f7">6</div><div class="sol-stat-label">Defined pipeline stages</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">4</div><div class="sol-stat-label">Live estimate statuses</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">Live</div><div class="sol-stat-label">Budget vs spend tracking</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">PDF</div><div class="sol-stat-label">One-click estimate export</div></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- PROJECTS MOCK -->
|
||||
<div class="mock-card">
|
||||
<img src="/page/assets/pipeline.png" alt="LynkedUp Pro Overview Dashboard" style="display:block;width:100%;height:auto" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══ CTA STRIP 2 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(2,14,10,.97) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(0,229,160,.07),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.22);color:var(--green)">
|
||||
<span class="strip-tdot" style="background:var(--green)"></span>
|
||||
Your margin doesn't vanish overnight - it leaks $50 at a time
|
||||
</div>
|
||||
<h2>See the Leak <span style="color:var(--green)">Before</span><br>It Becomes a <span style="color:var(--red)">Loss.</span></h2>
|
||||
<p>LynkedUp Pro flags budget overruns the moment your material spend hits 80% of budget, not after the job closes. Three caught overruns a month is thousands back in your pocket.</p>
|
||||
<!-- Before / After comparison -->
|
||||
<div style="display:grid;grid-template-columns:1fr auto 1fr;gap:12px;align-items:center;max-width:560px;margin:0 auto 30px;background:var(--card);border:1px solid var(--border);border-radius:14px;padding:22px 26px">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">Without LynkedUp</div>
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--red);line-height:1">After close</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px">When you find out the job lost money</div>
|
||||
</div>
|
||||
<div style="font-size:20px;color:var(--border2);text-align:center;padding:0 4px">vs</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">With LynkedUp Pro</div>
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--green);line-height:1">Live alert</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px">At 80% budget - while you can still act</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Protect My Margins Now →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> Live budget alerts</span>
|
||||
<span><b>✅</b> All 6 pipeline stages</span>
|
||||
<span><b>✅</b> Free onboarding</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
<section class="proof-section has-bg reveal" id="proof">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.88) 55%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:#a855f7;text-align:center">Real Impact</div>
|
||||
<h2 class="s-title" style="text-align:center">He Caught 3 Overruns<br>in One Month. <span style="color:#a855f7">That's Real</span><br><span style="color:var(--green)">Money Back.</span></h2>
|
||||
<div class="proof-card" style="border-color:rgba(139,92,246,.2)">
|
||||
<div class="proof-result" style="border-color:rgba(139,92,246,.2);background:rgba(139,92,246,.07)"><div class="pr-num" style="color:#a855f7">/bin/sh</div><div class="pr-label">Budget Surprises</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"Before LynkedUp Pro I was finding out jobs went over budget after they were done. Now the system flags it live. The minute our material spend hits 80% of budget I get a notification. We caught three jobs in the last month before they went negative. That's real money."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#a855f7,#0ea5e9)">JR</div>
|
||||
<div><div class="proof-name">Jason Rodriguez</div><div class="proof-role">Owner · Plano, TX</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(139,92,246,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div style="display:inline-block;background:rgba(139,92,246,.08);border:1px solid rgba(139,92,246,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:#a855f7;font-weight:600">📊 Every job. Every stage. Every dollar. Live.</div>
|
||||
<h2>Every Job on Your Board<br>Deserves a <em style="color:#a855f7">Live Number.</em><br>Not a Gut Feeling.</h2>
|
||||
<p>See the full Projects module - stages, estimate tracking, and live budget breakdown - in a free 30-minute demo.</p>
|
||||
<div class="cta-form"><a href="https://book.lynkeduppro.com/" class="btn-primary" onclick="event.preventDefault()">See Projects Module →</a></div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day guarantee</span><span>✅ Free onboarding</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer style="padding:32px 24px;border-top:1px solid var(--border);text-align:center"><p style="font-size:13px;color:var(--muted)">© 2026 LynkedUp Pro · <a href="lynkeduppro_final.html" style="color:var(--muted)">Full Platform →</a></p><p style="font-size:11px;color:var(--muted);opacity:.7;margin-top:10px;max-width:640px;margin-left:auto;margin-right:auto">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p></footer>
|
||||
<div class="mob-sticky"><a href="https://book.lynkeduppro.com/" class="p">Book a demo</a><a href="lynkeduppro_final.html" class="s">See All Features</a></div>
|
||||
<script>const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add('visible')})},{threshold:.1});document.querySelectorAll('.reveal').forEach(el=>obs.observe(el));</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,560 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:stretch;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.shot{display:block;width:100%;height:100%;border-radius:18px; object-fit:cover;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
/* ── background image helpers ── */
|
||||
.has-bg{position:relative;overflow:hidden}
|
||||
.has-bg>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.has-bg>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.has-bg>.container,.has-bg>.hero-bg,.has-bg>.eyebrow,.has-bg>h1,.has-bg>.hero-sub,.has-bg>.hero-actions,.has-bg>.micro-row,.has-bg>.stat-row{position:relative;z-index:2}
|
||||
/* ── bigger, punchier headings ── */
|
||||
.s-title{font-size:clamp(36px,5.2vw,64px);line-height:.96}
|
||||
/* ── CTA strip component ── */
|
||||
.cta-strip{padding:86px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.cta-strip>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.cta-strip>.glow{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:720px;height:380px;border-radius:50%;z-index:1;pointer-events:none}
|
||||
.cta-strip>.wrap{position:relative;z-index:2;max-width:700px;margin:0 auto}
|
||||
.strip-tag{display:inline-flex;align-items:center;gap:7px;padding:5px 16px;border-radius:100px;margin-bottom:22px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.strip-tdot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:blink 2s ease infinite}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5.2vw,62px);font-weight:900;line-height:.96;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.75;max-width:520px;margin:0 auto 30px}
|
||||
.strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.strip-trust{display:flex;gap:18px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.strip-trust b{color:var(--green)}
|
||||
/* ── comparison / stat cards in strips ── */
|
||||
.s-compare{display:grid;grid-template-columns:1fr auto 1fr;gap:14px;align-items:center;max-width:560px;margin:0 auto 30px;background:var(--card);border:1px solid var(--border);border-radius:14px;padding:22px 26px}
|
||||
.sc-side{text-align:center}
|
||||
.sc-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px}
|
||||
.sc-val{font-family:'Barlow Condensed',sans-serif;font-size:30px;font-weight:900;line-height:1}
|
||||
.sc-sub{font-size:11px;color:var(--muted);margin-top:5px}
|
||||
.sc-div{font-size:20px;color:var(--border2);text-align:center;padding:0 4px}
|
||||
.mpills{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:30px}
|
||||
.mpill{background:var(--card);border-radius:12px;padding:16px 20px;text-align:center;min-width:128px}
|
||||
.mpill-v{font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;line-height:1}
|
||||
.mpill-l{font-size:11px;color:var(--muted);margin-top:5px;text-transform:uppercase;letter-spacing:.4px;line-height:1.4}
|
||||
@media(max-width:1280px){section{padding:80px 24px}.cta-section{padding:80px 24px}.cta-strip{padding:76px 24px}}
|
||||
@media(max-width:1024px){nav{padding:14px 24px}section{padding:72px 22px}.cta-section{padding:72px 22px}.cta-strip{padding:68px 22px}.pain-grid{grid-template-columns:repeat(2,1fr)}.sol-split{gap:36px}h1{font-size:clamp(44px,7vw,76px)}.hero{padding:108px 20px 64px}.proof-card{padding:30px 26px}.proof-result{top:18px;right:18px}.stat-row{gap:28px;margin-top:48px}}
|
||||
@media(max-width:768px){nav{padding:12px 18px}section{padding:56px 18px}.cta-section{padding:56px 18px}.cta-strip{padding:56px 18px}.hero{min-height:auto;padding:96px 16px 56px}h1{font-size:clamp(40px,9vw,64px);line-height:.95}.hero-sub{font-size:15px}.s-title{font-size:clamp(30px,7vw,46px)}.stat-row{gap:18px;margin-top:40px}.stat+.stat::before{display:none}.stat-num{font-size:32px}.proof-card{padding:26px 22px;margin-top:32px}.proof-result{position:static;display:inline-block;margin-bottom:14px}.proof-quote{font-size:15px}.cta-section h2{font-size:clamp(30px,7vw,46px)}.cta-strip h2{font-size:clamp(28px,7vw,46px)}.sol-points li{font-size:14px}.p-item{padding:22px 18px}.mpills{gap:8px}.mpill{min-width:108px;padding:14px 16px}.mpill-v{font-size:22px}.s-compare{padding:18px 18px}.sc-val{font-size:24px}}
|
||||
@media(max-width:480px){nav{padding:10px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}section{padding:48px 16px}.cta-section{padding:48px 16px}.cta-strip{padding:48px 16px}.hero{padding:88px 14px 48px}h1{font-size:clamp(34px,10vw,52px);letter-spacing:-.5px}.hero-sub{font-size:14px;margin-top:18px}.s-title{font-size:clamp(28px,8vw,40px);letter-spacing:0}.s-sub{font-size:15px}.micro{font-size:12px}.micro-row{gap:10px}.stat-row{flex-direction:column;gap:18px;margin-top:32px}.stat-num{font-size:28px}.pain-grid{margin-top:30px}.p-item h3{font-size:15px}.p-item p{font-size:13px}.proof-card{padding:22px 18px;border-radius:14px}.proof-quote{font-size:14px;line-height:1.7}.proof-author{flex-wrap:wrap;gap:10px}.cta-section h2,.cta-strip h2{font-size:clamp(26px,8vw,38px)}.cta-section p,.cta-strip p{font-size:14px}.btn-primary,.btn-ghost{padding:12px 22px;font-size:15px}.sol-stats{grid-template-columns:1fr 1fr;gap:8px}.sol-stat{padding:12px}.sol-stat-num{font-size:22px}.mock-card{border-radius:14px}.mpills{flex-wrap:wrap}.mpill{min-width:86px;padding:12px 14px}.mpill-v{font-size:20px}.cta-micro{gap:8px;font-size:11px}.strip-tag{font-size:10px;padding:4px 12px;letter-spacing:.8px}footer{padding:32px 16px 24px}footer p{font-size:12px}.mob-sticky{padding:10px 12px}.mob-sticky a{font-size:13px;padding:11px}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}}
|
||||
@media(max-width:360px){h1{font-size:32px}.s-title{font-size:26px}.cta-section h2,.cta-strip h2{font-size:24px}.btn-primary,.btn-ghost{padding:11px 18px;font-size:14px}.sol-stats{grid-template-columns:1fr}}
|
||||
@media(max-width:860px){.mob-sticky{display:flex}body{padding-bottom:72px}.sol-split{grid-template-columns:1fr!important}.pain-grid{grid-template-columns:1fr!important}.s-compare{grid-template-columns:1fr;gap:8px}.sc-div{display:none}.shot{height:auto}}
|
||||
@media(max-width:768px){.stat+.stat::before{display:none}.stat-row{gap:18px}}
|
||||
@media(max-width:560px){.sol-stats{grid-template-columns:1fr 1fr}.stat-row{flex-direction:column;gap:18px}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin:24px auto 0}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}}
|
||||
@media(max-width:360px){.sol-stats{grid-template-columns:1fr}}</style>
|
||||
<title>LynkedUp Pro - Stop Losing Jobs to Double-Bookings and No-Shows</title>
|
||||
<meta name="description" content="AI-powered smart scheduling syncs with Google, iOS, and Calendly. Suggests optimal windows, prevents double-bookings, and gets your field team on time every time."/>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a>
|
||||
<a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a>
|
||||
</nav>
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1800&q=80');filter:brightness(.12) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(5,8,15,.5) 0%,rgba(5,8,15,.82) 55%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(245,158,11,.1),transparent 65%)"></div></div>
|
||||
<div class="eyebrow"><span class="dot" style="background:var(--orange)"></span> Smart Calendar · AI Scheduling · Field Intelligence</div>
|
||||
<h1>The Appointment<br>Was There. Your Rep<br><span style="color:var(--orange)">Wasn't. Job Gone.</span></h1>
|
||||
<p class="hero-sub">Double-bookings. Late arrivals. Reps walking in blind. <strong>Every scheduling failure hands a job to your competitor</strong>, and the homeowner never calls you back. It's your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Stop Losing Appointments →</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row" style="margin-top:18px">
|
||||
<span class="micro" style="color:var(--muted)"><span style="color:var(--orange)">4.9★</span> · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>✅</span> Syncs iOS, Google, Calendly</span>
|
||||
<span class="micro"><span>✅</span> AI suggests best times</span>
|
||||
<span class="micro"><span>✅</span> 98% on-track rate</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">98%</div><div class="stat-label">On-track rate this week</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">3</div><div class="stat-label">Calendars synced in real-time</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">0</div><div class="stat-label">Double-bookings since launch</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">AI</div><div class="stat-label">Suggests optimal windows</div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=1800&q=80');filter:brightness(.1) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(11,18,32,.98) 0%,rgba(11,18,32,.92) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">2-3 Lost Jobs a Month.<br>Same Cause <span style="color:var(--red)">Every Time.</span></h2>
|
||||
<p class="s-sub">It's never the product. It's never the price. It's the rep who was late, double-booked, or walked in with nothing to say. Scheduling failures are silent revenue killers, and they're completely preventable.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item"><div class="p-icon">📅</div><h3>Double-Bookings</h3><p>Rep A and Rep B both confirm the same homeowner for different times. The homeowner gets two calls, loses confidence in your operation, and calls someone else.</p><span class="p-cost">Cost: 1-2 leads lost per week</span></div>
|
||||
<div class="p-item"><div class="p-icon">👻</div><h3>No-Shows and Late Arrivals</h3><p>Rep doesn't check traffic, gets stuck, shows up 30 minutes late with no warning. Homeowner already moved on. You don't find out until the job's gone.</p><span class="p-cost">Cost: Referral reputation damage</span></div>
|
||||
<div class="p-item"><div class="p-icon">🗒️</div><h3>Reps Without Context</h3><p>Rep shows up to a homeowner meeting with no notes on the property, the previous conversation, or the damage assessment. Starts from scratch in front of the customer.</p><span class="p-cost">Cost: Lower close rate on every visit</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ══ CTA STRIP 1 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1508739773434-c26b3d09e071?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(16,8,2,.97) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(245,158,11,.09),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.28);color:var(--orange)">
|
||||
<span class="strip-tdot" style="background:var(--orange)"></span>
|
||||
Every missed appointment has a price tag
|
||||
</div>
|
||||
<h2>That Missed Appointment<br>You Didn't Follow Up On?<br><span style="color:var(--orange)">It Was Worth $12,000.</span></h2>
|
||||
<p>At an average job size of $12,000, losing 2-3 appointments a month isn't a scheduling inconvenience. It's $24,000-$36,000 in annual revenue walking out the door. Let's close that gap in one demo.</p>
|
||||
<!-- Revenue loss calc pills -->
|
||||
<div class="mpills">
|
||||
<div class="mpill" style="border:1px solid rgba(239,68,68,.22)">
|
||||
<div class="mpill-v" style="color:var(--red)">2-3</div>
|
||||
<div class="mpill-l">Jobs lost per month to scheduling</div>
|
||||
</div>
|
||||
<div class="mpill" style="border:1px solid rgba(245,158,11,.22)">
|
||||
<div class="mpill-v" style="color:var(--orange)">$12K</div>
|
||||
<div class="mpill-l">Avg roofing job value</div>
|
||||
</div>
|
||||
<div class="mpill" style="border:1px solid rgba(239,68,68,.22)">
|
||||
<div class="mpill-v" style="color:var(--red)">$36K</div>
|
||||
<div class="mpill-l">Lost revenue per year</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Show Me How to Fix This →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> Free 30-min demo</span>
|
||||
<span><b>✅</b> No credit card</span>
|
||||
<span><b>✅</b> Set up same day</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1800&q=80');filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(90deg,rgba(5,8,15,1) 44%,rgba(5,8,15,.72) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--accent)">The Fix</div>
|
||||
<h2 class="s-title">On Time. Every Time.<br><span style="color:var(--accent)">With Everything</span><br>They Need to Close.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro's smart calendar syncs your whole team, prevents conflicts automatically, and nudges reps with AI suggestions, traffic, timing, job context, before they even ask.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">🔄</span><span><strong>Real-time two-way sync</strong> - iOS Calendar, Google Calendar, and Calendly all stay in sync automatically. One source of truth for your entire team.</span></li>
|
||||
<li><span class="ico">🧠</span><span><strong>AI-suggested windows</strong> - suggests optimal times, avoids back-to-back gaps, and checks traffic before recommending when your rep should leave.</span></li>
|
||||
<li><span class="ico">📋</span><span><strong>Full context per appointment</strong> - every rep sees the homeowner notes, damage photos, previous conversations, and property details before they arrive.</span></li>
|
||||
<li><span class="ico">🛡️</span><span><strong>You stay in control</strong> - every AI suggestion is a nudge, not an override. You approve every change. No automation you didn't ask for.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">98%</div><div class="sol-stat-label">On-track rate in beta</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">3</div><div class="sol-stat-label">Calendar integrations</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">AI</div><div class="sol-stat-label">Traffic-aware suggestions</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:#fff">0</div><div class="sol-stat-label">Double-bookings</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-card">
|
||||
<img src="/page/assets/calendar.png" alt="LynkedUp Pro Overview Dashboard" style="display:block;width:100%;height:auto" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ══ CTA STRIP 2 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(2,14,10,.97) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(0,200,255,.07),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(0,200,255,.08);border:1px solid rgba(0,200,255,.22);color:var(--accent)">
|
||||
<span class="strip-tdot" style="background:var(--accent)"></span>
|
||||
The rep who shows up prepared wins the job
|
||||
</div>
|
||||
<h2>Context Closes.<br><span style="color:var(--accent)">Showing Up Blind</span><br>Doesn't.</h2>
|
||||
<p>LynkedUp Pro feeds every rep the homeowner's damage history, previous conversations, and property notes before they knock. When your rep walks in knowing more than the homeowner expects, they close.</p>
|
||||
<!-- Before / After comparison -->
|
||||
<div class="s-compare">
|
||||
<div class="sc-side">
|
||||
<div class="sc-label">Without LynkedUp</div>
|
||||
<div class="sc-val" style="color:var(--red)">Walks in blind</div>
|
||||
<div class="sc-sub">No notes. Starts from scratch.<br>Homeowner senses it.</div>
|
||||
</div>
|
||||
<div class="sc-div">vs</div>
|
||||
<div class="sc-side">
|
||||
<div class="sc-label">With LynkedUp Pro</div>
|
||||
<div class="sc-val" style="color:var(--green)">Walks in ready</div>
|
||||
<div class="sc-sub">Full context. Feels like a<br>trusted advisor. Closes.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">See the Calendar in Action →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> iOS, Google & Calendly sync</span>
|
||||
<span><b>✅</b> AI-suggested windows</span>
|
||||
<span><b>✅</b> Full context per appointment</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="proof-section has-bg reveal" id="proof">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.88) 55%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--orange);text-align:center">Real Result</div>
|
||||
<h2 class="s-title" style="text-align:center">His Reps Stopped<br>Missing Appointments.<br><span style="color:var(--orange)">Close Rate Up 43%.</span></h2>
|
||||
<div class="proof-card" style="border-color:rgba(245,158,11,.2)">
|
||||
<div class="proof-result" style="border-color:rgba(245,158,11,.18);background:rgba(245,158,11,.06)"><div class="pr-num" style="color:var(--orange)">+43%</div><div class="pr-label">Close Rate</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"The AI calendar suggestions changed everything for my team. Reps never miss appointments, they always show up with context about the property, and the route nudges actually help them plan smarter days. Within 60 days our close rate jumped 43%. The calendar alone drove most of that."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#0ea5e9,#00e5a0)">JR</div>
|
||||
<div><div class="proof-name">Jason Rodriguez</div><div class="proof-role">Owner · Dallas, TX · 14-person crew</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(245,158,11,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div style="display:inline-block;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--orange);font-weight:600">📅 The average roofing crew loses 2-3 jobs/month to scheduling failures</div>
|
||||
<h2>Your Next Missed<br>Appointment Is Already<br><em>On the Calendar.</em></h2>
|
||||
<p>See the smart calendar in action - synced, AI-powered, and built to stop your best leads from signing with someone else first.</p>
|
||||
<div class="cta-form">
|
||||
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Book a demo →</a>
|
||||
</div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day guarantee</span><span>✅ Works with your existing calendar</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<p>© 2026 LynkedUp Pro See Full Platform <a href="https://book.lynkeduppro.com/">Privacy</a> · <a href="https://book.lynkeduppro.com/">Terms</a></p>
|
||||
<p style="margin-top:8px;font-size:12px;opacity:.5">$349/mo flat rate · No credit card required · 30-day money-back guarantee · Built for roofing</p>
|
||||
<p style="margin-top:8px;font-size:11px;opacity:.5">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p>
|
||||
</footer>
|
||||
<div class="mob-sticky">
|
||||
<a href="https://book.lynkeduppro.com/" class="p">Book a demo</a>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add("visible")})},{threshold:.1});
|
||||
document.querySelectorAll(".reveal").forEach(el=>obs.observe(el));
|
||||
document.querySelectorAll("a[href^=\"#\"]").forEach(a=>{a.addEventListener("click",function(e){const t=document.querySelector(this.getAttribute("href"));if(t){e.preventDefault();t.scrollIntoView({behavior:"smooth",block:"start"});}}); });
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,635 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:stretch;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.shot{display:block;width:100%;height:100%;border-radius:18px;object-fit:cover;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
|
||||
@media(max-width:1280px){
|
||||
nav{padding:14px 32px}
|
||||
section{padding:80px 24px}
|
||||
.cta-strip{padding:72px 24px}
|
||||
.cta-section{padding:80px 24px}
|
||||
.container{max-width:100%}
|
||||
}
|
||||
@media(max-width:1024px){
|
||||
nav{padding:14px 24px}
|
||||
.pain-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.sol-split{gap:36px}
|
||||
.hero{min-height:auto;padding:120px 24px 72px}
|
||||
h1{font-size:clamp(40px,7vw,72px)}
|
||||
.s-title{font-size:clamp(32px,5vw,52px)!important}
|
||||
.stat-row{gap:28px}
|
||||
.proof-result{position:static;display:inline-block;margin-bottom:14px}
|
||||
}
|
||||
@media(max-width:860px){
|
||||
nav{padding:14px 20px}
|
||||
.sol-split{grid-template-columns:1fr!important}
|
||||
.pain-grid{grid-template-columns:1fr!important}
|
||||
.sol-stats{grid-template-columns:1fr 1fr}
|
||||
.mob-sticky{display:flex}
|
||||
body{padding-bottom:72px}
|
||||
section{padding:64px 20px}
|
||||
.cta-strip{padding:60px 20px}
|
||||
.cta-section{padding:64px 20px}
|
||||
.hero{padding:100px 16px 60px}
|
||||
.shot{height:auto}
|
||||
}
|
||||
@media(max-width:768px){
|
||||
nav{padding:12px 16px}
|
||||
.nav-cta{padding:8px 16px;font-size:13px}
|
||||
.logo-text{font-size:16px}
|
||||
.logo-mark{width:28px;height:28px;font-size:14px}
|
||||
section{padding:56px 18px}
|
||||
.cta-strip{padding:56px 18px}
|
||||
.cta-section{padding:56px 18px}
|
||||
.hero{padding:100px 16px 56px}
|
||||
.btn-primary,.btn-ghost{padding:12px 22px;font-size:15px}
|
||||
.stat-row{gap:20px;margin-top:44px}
|
||||
.stat+.stat::before{display:none}
|
||||
.stat-num{font-size:32px}
|
||||
.proof-quote{font-size:15px;line-height:1.7}
|
||||
.proof-card{padding:24px 20px}
|
||||
.cta-section h2{font-size:clamp(28px,7vw,48px)}
|
||||
.smet-row{gap:8px}
|
||||
.smet{min-width:0;flex:1 0 calc(50% - 8px);padding:12px}
|
||||
.smet-v{font-size:22px}
|
||||
.strip-trust{gap:12px}
|
||||
}
|
||||
@media(max-width:560px){
|
||||
.stat-row{gap:16px}
|
||||
h1{font-size:clamp(36px,11vw,64px);line-height:.96}
|
||||
.cta-form{flex-direction:column}
|
||||
.cta-form input{min-width:100%;width:100%}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
nav{padding:10px 14px}
|
||||
.logo-text{font-size:15px}
|
||||
.nav-cta{padding:7px 13px;font-size:12px}
|
||||
section{padding:44px 16px}
|
||||
.cta-strip{padding:48px 16px}
|
||||
.cta-section{padding:48px 16px}
|
||||
.hero{padding:90px 14px 48px}
|
||||
h1{font-size:clamp(32px,11vw,56px);line-height:.96}
|
||||
.hero-sub{font-size:15px;margin-top:18px}
|
||||
.hero-actions{flex-direction:column;align-items:stretch;margin-top:28px}
|
||||
.hero-actions a{width:100%;text-align:center}
|
||||
.s-title{font-size:clamp(26px,8vw,42px)!important}
|
||||
.s-sub{font-size:15px}
|
||||
.stat-row{gap:14px;margin-top:32px}
|
||||
.stat-num{font-size:24px}
|
||||
.stat-label{font-size:11px}
|
||||
.sol-stats{grid-template-columns:1fr}
|
||||
.cta-form .btn-primary{width:100%;text-align:center}
|
||||
.cta-section h2{font-size:clamp(24px,8vw,38px)}
|
||||
.cta-section p{font-size:15px}
|
||||
.cta-micro{gap:8px;font-size:11px}
|
||||
.strip-btns{flex-direction:column;align-items:stretch}
|
||||
.strip-btns a{width:100%;text-align:center}
|
||||
.strip-trust{flex-direction:column;align-items:center;gap:8px}
|
||||
.smet{flex:1 0 calc(50% - 8px)}
|
||||
.smet-v{font-size:20px}
|
||||
.smet-l{font-size:10px}
|
||||
.proof-card{padding:22px 16px;margin-top:32px}
|
||||
.proof-quote{font-size:14px}
|
||||
.p-item{padding:22px 18px}
|
||||
.mock-card{box-shadow:0 14px 28px rgba(0,0,0,.4)}
|
||||
.mob-sticky{padding:10px 12px;gap:8px}
|
||||
.mob-sticky a{padding:11px 8px;font-size:13px}
|
||||
footer{padding:32px 16px 24px}
|
||||
footer p{font-size:12px}
|
||||
.eyebrow{font-size:11px;padding:5px 14px}
|
||||
}
|
||||
@media(max-width:360px){
|
||||
h1{font-size:clamp(28px,11vw,48px)}
|
||||
.s-title{font-size:clamp(22px,9vw,36px)!important}
|
||||
.btn-primary,.btn-ghost{padding:11px 16px;font-size:14px}
|
||||
.stat-num{font-size:20px}
|
||||
.smet{flex:1 0 100%}
|
||||
}
|
||||
|
||||
/* ── bg helpers (proven v3) ── */
|
||||
.bg-img{position:absolute!important;inset:0!important;z-index:0!important;background-size:cover!important;background-position:center!important;pointer-events:none}
|
||||
.bg-ov{position:absolute!important;inset:0!important;z-index:1!important;pointer-events:none}
|
||||
.has-bg{position:relative!important;overflow:hidden!important}
|
||||
.has-bg>.container,.has-bg>.hero-bg,.has-bg>.eyebrow,
|
||||
.has-bg>h1,.has-bg>.hero-sub,.has-bg>.hero-actions,
|
||||
.has-bg>.micro-row,.has-bg>.stat-row,.has-bg>.wrap{position:relative!important;z-index:2!important}
|
||||
/* ── CTA strip ── */
|
||||
.cta-strip{padding:84px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip>.wrap{position:relative;z-index:2;max-width:700px;margin:0 auto}
|
||||
.strip-tag{display:inline-flex;align-items:center;gap:7px;padding:5px 16px;border-radius:100px;margin-bottom:22px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.strip-tdot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:blink 2s ease infinite}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5vw,60px);font-weight:900;line-height:.97;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.72;max-width:520px;margin:0 auto 28px}
|
||||
.strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.strip-trust{display:flex;gap:18px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.strip-trust b{color:var(--green)}
|
||||
.smet-row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:28px}
|
||||
.smet{background:var(--card);border-radius:12px;padding:14px 20px;text-align:center;min-width:118px}
|
||||
.smet-v{font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;line-height:1}
|
||||
.smet-l{font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.4px;line-height:1.4}
|
||||
/* ── bigger s-title ── */
|
||||
.s-title{font-size:clamp(36px,5vw,62px)!important;line-height:.97!important}
|
||||
|
||||
</style>
|
||||
<title>LynkedUp Pro - Stop Losing Jobs When Hail Hits</title>
|
||||
<meta name="description" content="Real-time hail alerts route your reps to the highest-damage doors within minutes of a storm. Beat competitors before they know it rained."/>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a>
|
||||
<a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504309092620-4d0ec726efa4?w=1800&q=80');filter:brightness(.12) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(5,8,15,.45) 0%,rgba(5,8,15,.8) 55%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg">
|
||||
<div class="hero-grid"></div>
|
||||
<div class="hero-glow" style="background:radial-gradient(ellipse,rgba(239,68,68,.1),transparent 65%)"></div>
|
||||
</div>
|
||||
<div class="eyebrow"><span class="dot" style="background:var(--red)"></span> Storm Intelligence · Real-Time Hail Alerts</div>
|
||||
<h1>While You Check<br>the Weather App,<br><span style="color:var(--red)">They Sign Contracts.</span></h1>
|
||||
<p class="hero-sub">By the time you hear about a hail event, the best crews in your market have knocked 40 doors and signed 10 contracts. <strong>LynkedUp Pro puts you there first</strong> - your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">See It Stop a Storm Loss →</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row" style="margin-top:18px">
|
||||
<span class="micro" style="color:var(--orange)">4.9★ · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>⚡</span> Sub-hour hail alerts</span>
|
||||
<span class="micro"><span>🗺️</span> AI address scoring</span>
|
||||
<span class="micro"><span>✅</span> 30-day guarantee</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--red)">17 min</div><div class="stat-label">Storm alert → reps on doors</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">89</div><div class="stat-label">Addresses scored per event</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">11</div><div class="stat-label">Contracts signed same day</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">1st</div><div class="stat-label">On scene before competition</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- THE PAIN -->
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1527689368864-3a821dbccc34?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(11,18,32,.98) 0%,rgba(11,18,32,.93) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">Second Place<br>After a Storm Is<br><span style="color:var(--red)">Worth $0.</span></h2>
|
||||
<p class="s-sub">It's not about effort. It's about intelligence. The crews winning after every storm have one thing you don't. They know about it first.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item">
|
||||
<div class="p-icon">🌩️</div>
|
||||
<h3>You Hear About It Late</h3>
|
||||
<p>Weather apps, neighbor texts, news alerts. By the time you piece it together, the storm swath is already being worked. You're not slow, you're uninformed.</p>
|
||||
<span class="p-cost">Cost: 3-5 lost jobs per storm</span>
|
||||
</div>
|
||||
<div class="p-item">
|
||||
<div class="p-icon">🗺️</div>
|
||||
<h3>No Map, No Priority</h3>
|
||||
<p>Even when you know it hailed, which street do you hit first? Without damage scoring, your reps knock random doors while competitors work the worst-hit blocks.</p>
|
||||
<span class="p-cost">Cost: Wasted canvassing hours</span>
|
||||
</div>
|
||||
<div class="p-item">
|
||||
<div class="p-icon">🚗</div>
|
||||
<h3>Reps Route Themselves</h3>
|
||||
<p>No dispatch. No territory assignment. Reps show up wherever they feel like, overlap each other, miss entire neighborhoods, and leave money on the ground.</p>
|
||||
<span class="p-cost">Cost: Hundreds of doors unmissed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- THE SOLUTION -->
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
<!-- ══ CTA STRIP ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1800&q=80');filter:brightness(.09) saturate(.38)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(16,4,4,.97) 100%)"></div>
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:360px;border-radius:50%;background:radial-gradient(ellipse,rgba(239,68,68,.07),transparent 70%);z-index:1;pointer-events:none"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.28);color:var(--red)">
|
||||
<span class="strip-tdot" style="background:var(--red)"></span>
|
||||
Right now, somewhere in your territory
|
||||
</div>
|
||||
<h2>Hail Fell.<br><span style="color:var(--red)">Is Anyone</span><br><span style="color:var(--orange)">Moving?</span></h2>
|
||||
<p>A competitor using LynkedUp Pro already has the swath mapped, the top addresses scored, and three reps auto-routed to the highest-damage doors. The window to be first is 17 minutes. Not hours.</p>
|
||||
<div class="smet-row"><div class="smet" style="border:1px solid rgba(239,68,68,.22)"><div class="smet-v" style="color:var(--red)"><1hr</div><div class="smet-l">Alert to reps deployed</div></div><div class="smet" style="border:1px solid rgba(245,158,11,.22)"><div class="smet-v" style="color:var(--orange)">17 min</div><div class="smet-l">Avg storm → dispatch</div></div><div class="smet" style="border:1px solid rgba(0,229,160,.22)"><div class="smet-v" style="color:var(--green)">11</div><div class="smet-l">Contracts same day</div></div><div class="smet" style="border:1px solid rgba(0,200,255,.22)"><div class="smet-v" style="color:var(--accent)">89</div><div class="smet-l">Addresses scored</div></div></div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Show Me How to Be First →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> No credit card</span>
|
||||
<span><b>✅</b> Free 30-min demo</span>
|
||||
<span><b>✅</b> 30-day guarantee</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(90deg,rgba(5,8,15,1) 42%,rgba(5,8,15,.75) 100%)"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div class="s-label" style="color:var(--accent)">The Fix</div>
|
||||
<h2 class="s-title">17 Minutes.<br><span style="color:var(--accent)">Reps Deployed.</span><br>Doors Getting Knocked.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro pulls storm data from HailTrace and HailWatch the moment a hail event is detected. Every address in the impact zone gets an AI damage score. Your reps get their route.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">📡</span><span><strong>Dual-source hail data.</strong> HailTrace + HailWatch fusion means you never miss an event in your territory, day or night.</span></li>
|
||||
<li><span class="ico">🎯</span><span><strong>AI address scoring.</strong> Every home ranked HIGH / MED / LOW by hail severity. Your reps hit the best doors first, every time.</span></li>
|
||||
<li><span class="ico">📲</span><span><strong>Auto-dispatch to field.</strong> Optimized canvassing routes pushed directly to your reps' phones within minutes of a storm ending.</span></li>
|
||||
<li><span class="ico">📎</span><span><strong>LiDAR scan on arrival.</strong> Rep deploys drone, AI builds digital twin and flags every impact zone in under 4 minutes. Proposal ready before competitors arrive.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--red)"><1 hr</div><div class="sol-stat-label">Alert to reps deployed</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">89</div><div class="sol-stat-label">Addresses scored per storm</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">2</div><div class="sol-stat-label">Storm data sources fused</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">$0</div><div class="sol-stat-label">Extra cost for hail intel</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- HAIL MAP MOCK -->
|
||||
<div class="mock-card">
|
||||
<img src="/page/assets/storm.png" alt="LynkedUp Pro Overview Dashboard" style="display:block;width:100%;height:auto" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROOF -->
|
||||
<section class="proof-section has-bg reveal">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.88) 60%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--green);text-align:center">Real Result</div>
|
||||
<h2 class="s-title" style="text-align:center">Alert at 2:14PM.<br><span style="color:var(--green)">11 Contracts</span><br>Before Dark.</h2>
|
||||
<div class="proof-card">
|
||||
<div class="proof-result"><div class="pr-num">17 min</div><div class="pr-label">Storm→Doors</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"Alert fired at 2:14 PM. My three reps were knocking doors by 2:31. By end of day we had 11 signed contracts. Our closest competitor showed up the next morning. That is literally the difference this makes, and that was week one."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#a855f7,#0ea5e9)">SA</div>
|
||||
<div><div class="proof-name">Sarah Alonzo</div><div class="proof-role">Operations Manager · Houston, TX</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(245,158,11,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:1">
|
||||
<div style="display:inline-block;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--red);font-weight:600">⚡ The next storm in your territory could hit today</div>
|
||||
<h2>The Next Storm<br>Could Hit <em>Today.</em><br>Will You Be First?</h2>
|
||||
<p>Stop losing jobs to crews who just got lucky with timing. LynkedUp Pro makes luck irrelevant.</p>
|
||||
<div class="cta-form">
|
||||
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Book a demo →</a>
|
||||
</div>
|
||||
<div class="cta-micro">
|
||||
<span>✅ No credit card</span><span>✅ 30-day money-back guarantee</span><span>✅ Free onboarding</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
@keyframes hpulse{0%,100%{transform:scale(1);opacity:.9}50%{transform:scale(1.35);opacity:.45}}
|
||||
@keyframes hblink{0%,100%{opacity:1}50%{opacity:.55}}
|
||||
h1 .red{color:var(--red)}
|
||||
</style>
|
||||
<footer>
|
||||
<p>© 2026 LynkedUp Pro See Full Platform →<a href="https://book.lynkeduppro.com/">Privacy</a> · <a href="https://book.lynkeduppro.com/">Terms</a></p>
|
||||
<p style="margin-top:8px;font-size:12px;opacity:.5">$2000/mo flat rate · No credit card required · 30-day money-back guarantee · Built for roofing</p>
|
||||
<p style="margin-top:8px;font-size:11px;opacity:.5">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p>
|
||||
</footer>
|
||||
<div class="mob-sticky">
|
||||
<a href="https://book.lynkeduppro.com/" class="p">Book a demo</a>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add("visible")})},{threshold:.1});
|
||||
document.querySelectorAll(".reveal").forEach(el=>obs.observe(el));
|
||||
document.querySelectorAll("a[href^=\"#\"]").forEach(a=>{a.addEventListener("click",function(e){const t=document.querySelector(this.getAttribute("href"));if(t){e.preventDefault();t.scrollIntoView({behavior:"smooth",block:"start"});}}); });
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,426 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
/* ── background image helpers ── */
|
||||
.has-bg{position:relative;overflow:hidden}
|
||||
.has-bg>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.has-bg>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.has-bg>.container,.has-bg>.hero-bg,.has-bg>.eyebrow,.has-bg>h1,.has-bg>.hero-sub,.has-bg>.hero-actions,.has-bg>.micro-row,.has-bg>.stat-row{position:relative;z-index:2}
|
||||
/* ── bigger s-title ── */
|
||||
.s-title{font-size:clamp(36px,5vw,62px);line-height:.97}
|
||||
/* ── CTA strip ── */
|
||||
.cta-strip{padding:84px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip>.bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0;pointer-events:none}
|
||||
.cta-strip>.bg-ov{position:absolute;inset:0;z-index:1;pointer-events:none}
|
||||
.cta-strip>.glow{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:360px;border-radius:50%;z-index:1;pointer-events:none}
|
||||
.cta-strip>.wrap{position:relative;z-index:2;max-width:680px;margin:0 auto}
|
||||
.strip-tag{display:inline-flex;align-items:center;gap:7px;padding:5px 16px;border-radius:100px;margin-bottom:22px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.strip-tag .tdot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:blink 2s ease infinite}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5vw,60px);font-weight:900;line-height:.97;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.72;max-width:500px;margin:0 auto 28px}
|
||||
.strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.strip-trust{display:flex;gap:18px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.strip-trust span b{color:var(--green)}
|
||||
/* ── savings compare cards ── */
|
||||
.save-cards{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:30px}
|
||||
.save-card{background:var(--card);border-radius:12px;padding:16px 22px;text-align:center;min-width:120px}
|
||||
.save-card .sv{font-family:'Barlow Condensed',sans-serif;font-size:30px;font-weight:900;line-height:1}
|
||||
.save-card .sl{font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px}
|
||||
@media(max-width:1280px){section{padding:80px 24px}.cta-section{padding:80px 24px}.cta-strip{padding:76px 24px}}
|
||||
@media(max-width:1024px){nav{padding:14px 24px}section{padding:72px 22px}.cta-section{padding:72px 22px}.cta-strip{padding:68px 22px}.pain-grid{grid-template-columns:repeat(2,1fr)}.sol-split{gap:36px}h1{font-size:clamp(44px,7vw,76px)}.hero{padding:108px 20px 64px}.proof-card{padding:30px 26px}.proof-result{top:18px;right:18px}.stat-row{gap:28px;margin-top:48px}}
|
||||
@media(max-width:768px){nav{padding:12px 18px}section{padding:56px 18px}.cta-section{padding:56px 18px}.cta-strip{padding:56px 18px}.hero{min-height:auto;padding:96px 16px 56px}h1{font-size:clamp(40px,9vw,64px);line-height:.95}.hero-sub{font-size:15px}.s-title{font-size:clamp(30px,7vw,46px)}.stat-row{gap:18px;margin-top:40px}.stat+.stat::before{display:none}.stat-num{font-size:32px}.proof-card{padding:26px 22px;margin-top:32px}.proof-result{position:static;display:inline-block;margin-bottom:14px}.proof-quote{font-size:15px}.cta-section h2{font-size:clamp(30px,7vw,46px)}.cta-strip h2{font-size:clamp(28px,7vw,46px)}.sol-points li{font-size:14px}.p-item{padding:22px 18px}.save-cards{gap:8px}.save-card{min-width:96px;padding:14px 16px}.save-card .sv{font-size:24px}}
|
||||
@media(max-width:480px){nav{padding:10px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}section{padding:48px 16px}.cta-section{padding:48px 16px}.cta-strip{padding:48px 16px}.hero{padding:88px 14px 48px}h1{font-size:clamp(34px,10vw,52px);letter-spacing:-.5px}.hero-sub{font-size:14px;margin-top:18px}.s-title{font-size:clamp(28px,8vw,40px);letter-spacing:0}.s-sub{font-size:15px}.micro{font-size:12px}.micro-row{gap:10px}.stat-row{flex-direction:column;gap:18px;margin-top:32px}.stat-num{font-size:28px}.pain-grid{margin-top:30px}.p-item h3{font-size:15px}.p-item p{font-size:13px}.proof-card{padding:22px 18px;border-radius:14px}.proof-quote{font-size:14px;line-height:1.7}.proof-author{flex-wrap:wrap;gap:10px}.cta-section h2,.cta-strip h2{font-size:clamp(26px,8vw,38px)}.cta-section p,.cta-strip p{font-size:14px}.btn-primary,.btn-ghost{padding:12px 22px;font-size:15px}.sol-stats{grid-template-columns:1fr 1fr;gap:8px}.sol-stat{padding:12px}.sol-stat-num{font-size:22px}.mock-card{border-radius:14px}.save-cards{flex-wrap:wrap}.save-card{min-width:80px;padding:12px 14px}.save-card .sv{font-size:22px}.cta-micro{gap:8px;font-size:11px}.strip-tag{font-size:10px;padding:4px 12px;letter-spacing:.8px}footer{padding:32px 16px 24px}footer p{font-size:12px}.mob-sticky{padding:10px 12px}.mob-sticky a{font-size:13px;padding:11px}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}}
|
||||
@media(max-width:360px){h1{font-size:32px}.s-title{font-size:26px}.cta-section h2,.cta-strip h2{font-size:24px}.btn-primary,.btn-ghost{padding:11px 18px;font-size:14px}.sol-stats{grid-template-columns:1fr}}
|
||||
@media(max-width:860px){.mob-sticky{display:flex}body{padding-bottom:72px}.sol-split{grid-template-columns:1fr!important}.pain-grid{grid-template-columns:1fr!important}}
|
||||
@media(max-width:768px){.stat+.stat::before{display:none}.stat-row{gap:18px}}
|
||||
@media(max-width:560px){.sol-stats{grid-template-columns:1fr 1fr}.stat-row{flex-direction:column;gap:18px}.hero-actions{flex-direction:column;align-items:stretch;max-width:300px;margin:24px auto 0}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;align-items:stretch;max-width:300px;margin-left:auto;margin-right:auto}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}.cta-form{flex-direction:column}.cta-form input{width:100%;min-width:100%}.cta-form .btn-primary{width:100%;text-align:center}}
|
||||
@media(max-width:360px){.sol-stats{grid-template-columns:1fr}}</style>
|
||||
<title>LynkedUp Pro - Kill Your EagleView Bill. Scan in Minutes.</title>
|
||||
<meta name="description" content="Stop paying $15-$50 per EagleView report and waiting 48 hours. LynkedUp Pro AI measurements scan any roof in minutes for $0 per report, included in your flat $349/mo."/>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a>
|
||||
<a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a>
|
||||
</nav>
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1473968512647-3e447244af8f?w=1800&q=80');filter:brightness(.12) saturate(.45)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(5,8,15,.55) 0%,rgba(5,8,15,.82) 55%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(0,200,255,.11),transparent 65%)"></div></div>
|
||||
<div class="eyebrow" style="position:relative;z-index:2"><span class="dot"></span> AI Measurements · LiDAR · $0/Report</div>
|
||||
<h1 style="position:relative;z-index:2">While You Wait<br><span style="color:var(--accent)">48 Hours,</span><br>They Sign Someone Else.</h1>
|
||||
<p class="hero-sub" style="position:relative;z-index:2">EagleView makes you pay <strong>$15-$50 per report</strong> and wait two days. That's two days for a competitor to fly their own drone, hand the homeowner a quote, and close the job that was yours. LynkedUp Pro is your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions" style="position:relative;z-index:2">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">See What You'd Save →</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row" style="position:relative;z-index:2">
|
||||
<span class="micro" style="color:var(--muted)">4.9★ · 248 reviews</span>
|
||||
<span class="micro"><span>✅</span> $0 per report - always</span>
|
||||
<span class="micro"><span>✅</span> Scan in minutes on-site</span>
|
||||
<span class="micro"><span>✅</span> Insurance-grade accuracy</span>
|
||||
</div>
|
||||
<div class="stat-row" style="position:relative;z-index:2">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">$0</div><div class="stat-label">Per report fee. Ever.</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">10×</div><div class="stat-label">Faster than EagleView</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">$700+</div><div class="stat-label">Avg monthly EagleView bill</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">48 hrs</div><div class="stat-label">Wait time eliminated</div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=1800&q=80');filter:brightness(.1) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(11,18,32,.98) 0%,rgba(11,18,32,.9) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">$700 a Month<br>to <span style="color:var(--red)">Lose Jobs Faster.</span><br>That's the EagleView Deal.</h2>
|
||||
<p class="s-sub">You're funding a per-report model built for a world before drones and AI existed. Every slow turnaround and every invoice is a self-inflicted wound on your close rate.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item"><div class="p-icon">💸</div><h3>$700+ a Month in Report Fees</h3><p>20 reports at $35 each. 30 reports at $50 each. Active storm crews easily spend $700-$2,000 per month before any other software cost.</p><span class="p-cost">Cost: $8,400-$24,000/year</span></div>
|
||||
<div class="p-item"><div class="p-icon">⏳</div><h3>48-Hour Turnaround</h3><p>You order the report, wait two days, then call the homeowner back. By then, two other roofers have already been on the roof and one has a signed contract.</p><span class="p-cost">Cost: 1 in 3 leads lost to speed</span></div>
|
||||
<div class="p-item"><div class="p-icon">🔗</div><h3>Disconnected from Your Workflow</h3><p>EagleView report arrives as a PDF. You manually copy measurements into your estimate tool, then into your proposal. Three steps that should be zero.</p><span class="p-cost">Cost: 45+ min per job wasted</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ══ CTA STRIP 1 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(18,8,2,.96) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(245,158,11,.08),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.25);color:var(--orange)">
|
||||
<span class="tdot" style="background:var(--orange)"></span>
|
||||
Your money is leaving every time you order a report
|
||||
</div>
|
||||
<h2>Every Job You Quote<br>Costs <span style="color:var(--orange)">$35 Before You</span><br>Say a Word. <span style="color:var(--red)">Stop.</span></h2>
|
||||
<p>Run 20 jobs a month? That's $700 gone before a single shingle is sold. LynkedUp Pro scans the same roof in 4 minutes for exactly $0.00, and the report auto-fills your estimate.</p>
|
||||
<div class="save-cards">
|
||||
<div class="save-card" style="border:1px solid rgba(239,68,68,.2)">
|
||||
<div class="sv" style="color:var(--red)">$700+</div>
|
||||
<div class="sl">EagleView / mo</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--muted);padding:0 4px">→</div>
|
||||
<div class="save-card" style="border:1px solid rgba(0,229,160,.2)">
|
||||
<div class="sv" style="color:var(--green)">$0.00</div>
|
||||
<div class="sl">LynkedUp Pro / mo</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;font-size:28px;color:var(--muted);padding:0 4px">=</div>
|
||||
<div class="save-card" style="border:1px solid rgba(0,200,255,.2)">
|
||||
<div class="sv" style="color:var(--accent)">$8.4K+</div>
|
||||
<div class="sl">Back in your pocket / yr</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Calculate My Exact Savings →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> No per-report fees ever</span>
|
||||
<span><b>✅</b> Free 30-min demo</span>
|
||||
<span><b>✅</b> Switch in one day</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=1800&q=80');filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(90deg,rgba(5,8,15,1) 42%,rgba(5,8,15,.7) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--green)">The Fix</div>
|
||||
<h2 class="s-title">Scan It. Quote It.<br>Close It. <span style="color:var(--green)">Before You</span><br><span style="color:var(--accent)">Leave the Driveway.</span></h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro's AI measurement engine runs on your DJI drone. Fly the property, get centimeter-accurate measurements, and generate an insurance-grade report - all before you leave the driveway.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">🎯</span><span><strong>Sub-centimeter precision</strong> - AI + LiDAR fusion delivers measurement accuracy insurance adjusters accept on first submission.</span></li>
|
||||
<li><span class="ico">⚡</span><span><strong>Minutes on-site, not 48 hours later</strong> - fly the drone, get the numbers, hand the homeowner a proposal while you're still standing there.</span></li>
|
||||
<li><span class="ico">🔄</span><span><strong>Auto-fills your estimate</strong> - measurements feed directly into Canvas proposals. No manual re-entry. No errors. No wasted hour.</span></li>
|
||||
<li><span class="ico">📎</span><span><strong>Xactimate-style output</strong> - reports formatted exactly how adjusters expect them, with full photo documentation and storm data included.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">$0</div><div class="sol-stat-label">Per-report cost</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">~4 min</div><div class="sol-stat-label">Avg flight + scan time</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">10×</div><div class="sol-stat-label">Faster than waiting on EagleView</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:#fff">1st</div><div class="sol-stat-label">Submission approval rate</div></div>
|
||||
</div>
|
||||
<div style="margin-top:28px;background:rgba(0,229,160,.05);border:1px solid rgba(0,229,160,.18);border-radius:12px;padding:20px 22px">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:1px;margin-bottom:12px">Your Monthly Savings</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
|
||||
<div style="text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;color:var(--red)">$700+</div><div style="font-size:11px;color:var(--muted)">EagleView/mo</div></div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--muted)">→</div>
|
||||
<div style="text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;color:var(--green)">$0</div><div style="font-size:11px;color:var(--muted)">LynkedUp Pro/mo</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-card">
|
||||
<div class="mock-bar"><span class="mock-title">Project #1286 - AI Measurement</span><span class="mock-badge" style="background:rgba(0,200,255,.1);color:var(--accent);border:1px solid rgba(0,200,255,.25)">AI-POWERED</span></div>
|
||||
<div style="padding:14px;background:#070d18">
|
||||
<svg width="100%" viewBox="0 0 300 185" fill="none">
|
||||
<rect width="300" height="185" fill="#070d18"/>
|
||||
<polygon points="150,18 268,105 210,105 210,165 90,165 90,105 32,105" fill="rgba(0,200,255,.05)" stroke="rgba(0,200,255,.5)" stroke-width="1.2"/>
|
||||
<line x1="32" y1="110" x2="268" y2="110" stroke="rgba(0,200,255,.3)" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="150" y="122" fill="rgba(0,200,255,.9)" font-size="9" text-anchor="middle" font-family="monospace" font-weight="bold">27ft 6in</text>
|
||||
<rect x="168" y="20" width="122" height="85" rx="5" fill="rgba(7,13,24,.92)" stroke="rgba(0,200,255,.15)" stroke-width="1"/>
|
||||
<text x="229" y="35" fill="rgba(255,255,255,.4)" font-size="7" text-anchor="middle" font-family="monospace">TOTAL AREA</text>
|
||||
<text x="229" y="52" fill="#00c8ff" font-size="14" text-anchor="middle" font-family="monospace" font-weight="bold">2,347 sq ft</text>
|
||||
<text x="229" y="63" fill="rgba(255,255,255,.3)" font-size="6.5" text-anchor="middle" font-family="monospace">23.47 SQUARES</text>
|
||||
<text x="168" y="76" fill="rgba(255,255,255,.35)" font-size="6.5" font-family="monospace">PLANE 1</text><text x="282" y="76" fill="rgba(255,255,255,.6)" font-size="6.5" font-family="monospace" text-anchor="end">425 SQ FT</text>
|
||||
<text x="168" y="87" fill="rgba(255,255,255,.35)" font-size="6.5" font-family="monospace">PLANE 2</text><text x="282" y="87" fill="rgba(255,255,255,.6)" font-size="6.5" font-family="monospace" text-anchor="end">362 SQ FT</text>
|
||||
<text x="168" y="98" fill="rgba(255,255,255,.35)" font-size="6.5" font-family="monospace">PLANE 3+</text><text x="282" y="98" fill="#00c8ff" font-size="6.5" font-family="monospace" text-anchor="end" font-weight="bold">1,560 SQ FT</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div style="padding:12px 14px;border-top:1px solid var(--border)">
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;padding:5px 0;border-bottom:1px solid var(--border)"><span style="color:var(--muted)">Scan Time</span><span style="font-weight:600;color:#fff">~4 min</span></div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;padding:5px 0;border-bottom:1px solid var(--border)"><span style="color:var(--muted)">Planes Measured</span><span style="font-weight:600;color:#fff">14</span></div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;padding:8px 0;font-family:'Barlow Condensed',sans-serif;font-weight:800"><span style="color:var(--muted)">Report Cost</span><span style="color:var(--green)">$0.00</span></div>
|
||||
</div>
|
||||
<a href="https://book.lynkeduppro.com/" style="display:block;margin:0 12px 12px;background:var(--accent);color:#05080f;text-align:center;padding:9px;border-radius:7px;font-weight:700;font-size:13px;text-decoration:none">Generate Xactimate-Style Report →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ══ CTA STRIP 2 ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.96) 0%,rgba(2,14,10,.96) 100%)"></div>
|
||||
<div class="glow" style="background:radial-gradient(ellipse,rgba(0,229,160,.07),transparent 70%)"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.22);color:var(--green)">
|
||||
<span class="tdot" style="background:var(--green)"></span>
|
||||
The homeowner signs with whoever shows up first with numbers
|
||||
</div>
|
||||
<h2>Your Competitor<br>Is Already <span style="color:var(--green)">On That Roof.</span><br><span style="color:var(--accent)">Are You?</span></h2>
|
||||
<p>The job doesn't go to the best roofer. It goes to the roofer who hands over a professional proposal while still standing in the driveway. That's LynkedUp Pro, on every single job.</p>
|
||||
<!-- Speed comparison -->
|
||||
<div style="display:grid;grid-template-columns:1fr auto 1fr;gap:12px;align-items:center;max-width:540px;margin:0 auto 30px;background:var(--card);border:1px solid var(--border);border-radius:14px;padding:20px 24px">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">EagleView</div>
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:32px;font-weight:900;color:var(--red);line-height:1">48 hrs</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">to get measurements</div>
|
||||
</div>
|
||||
<div style="font-size:22px;color:var(--border2);text-align:center">vs</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px">LynkedUp Pro</div>
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:32px;font-weight:900;color:var(--green);line-height:1">4 min</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px">on-site, before you leave</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Start Closing Jobs Faster →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> $0 per report</span>
|
||||
<span><b>✅</b> Insurance-grade accuracy</span>
|
||||
<span><b>✅</b> Live in 24 hrs</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="proof-section has-bg reveal" id="proof">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1521791136064-7986c2920216?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.9) 55%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--green);text-align:center">Real Result</div>
|
||||
<h2 class="s-title" style="text-align:center">He Ditched EagleView<br>on a Tuesday. <span style="color:var(--green)">Saved<br>$2,100 by Friday.</span></h2>
|
||||
<div class="proof-card">
|
||||
<div class="proof-result"><div class="pr-num">$2.1K</div><div class="pr-label">Saved/Month</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"Cut EagleView, JobNimbus, and SalesRabbit in one move. LynkedUp Pro replaced all three for less money - and the measurements are actually better. Every insurance claim gets approved on the first submission now. I don't know why I waited."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MT</div>
|
||||
<div><div class="proof-name">Marcus Thompson</div><div class="proof-role">Operations Manager · Houston, TX</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(245,158,11,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div style="display:inline-block;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--green);font-weight:600">💰 EagleView charges per report. We never do.</div>
|
||||
<h2>Your Next Report<br>Costs <em>$0.</em><br>So Does Every One After.</h2>
|
||||
<p>One flat rate. Unlimited scans. See exactly how much you'd save - and how fast you'd close more jobs - in a free 30-minute demo.</p>
|
||||
<div class="cta-form">
|
||||
|
||||
|
||||
</div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day money-back guarantee</span><span>✅ Free onboarding call</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- LEGAL: Founders / pricing Terms & Conditions - no pricing section on this page, placed before footer -->
|
||||
<div class="legal-terms" style="max-width:820px;margin:26px auto 0;padding:16px 20px;border:1px solid rgba(255,255,255,0.10);border-radius:12px;background:rgba(255,255,255,0.02);text-align:left">
|
||||
<details>
|
||||
<summary style="cursor:pointer;font-size:12px;font-weight:700;letter-spacing:0.05em;color:#9aa4b2;list-style:none">Disclaimer, Terms & Conditions</summary>
|
||||
<div style="margin-top:12px;font-size:11px;line-height:1.65;color:#8b95a3">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<footer>
|
||||
<p>© 2026 LynkedUp Pro · <a href="lynkeduppro_final.html">See Full Platform →</a> · <a href="https://book.lynkeduppro.com/">Privacy</a> · <a href="https://book.lynkeduppro.com/">Terms</a></p>
|
||||
<p style="margin-top:8px;font-size:12px;opacity:.5">$349/mo flat rate · No credit card required · 30-day money-back guarantee · Built for roofing</p>
|
||||
<p style="margin-top:8px;font-size:11px;opacity:.5">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p>
|
||||
</footer>
|
||||
<div class="mob-sticky">
|
||||
<a href="https://book.lynkeduppro.com/" class="p">Book a demo</a>
|
||||
<a href="lynkeduppro_final.html" class="s">See Full Platform</a>
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add("visible")})},{threshold:.1});
|
||||
document.querySelectorAll(".reveal").forEach(el=>obs.observe(el));
|
||||
document.querySelectorAll("a[href^=\"#\"]").forEach(a=>{a.addEventListener("click",function(e){const t=document.querySelector(this.getAttribute("href"));if(t){e.preventDefault();t.scrollIntoView({behavior:"smooth",block:"start"});}}); });
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,516 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:stretch;margin-top:44px}
|
||||
.sol-split > div:last-child{display:flex;align-items:center;justify-content:center;height:100%}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
@media(max-width:860px){nav{padding:14px 20px}.sol-split{grid-template-columns:1fr!important}.pain-grid{grid-template-columns:1fr!important}.sol-stats{grid-template-columns:1fr 1fr}.mob-sticky{display:flex}body{padding-bottom:72px}section{padding:64px 20px}.hero{padding:100px 16px 60px}}
|
||||
@media(max-width:560px){.stat-row{gap:24px}h1{font-size:clamp(44px,13vw,80px)}.cta-form{flex-direction:column}.cta-form input{min-width:100%}}
|
||||
@media(max-width:1280px){nav{padding:16px 32px}section{padding:80px 24px}.cta-strip{padding:72px 24px}.cta-section{padding:80px 24px}}
|
||||
@media(max-width:1024px){nav{padding:14px 24px}section{padding:72px 20px}.sol-split{grid-template-columns:1fr;gap:40px}.pain-grid{grid-template-columns:repeat(2,1fr)}.hero{padding:110px 20px 70px}h1{font-size:clamp(44px,7vw,76px)}.stat-row{gap:28px}.proof-card{padding:30px}}
|
||||
@media(max-width:768px){nav{padding:12px 16px}.nav-cta{padding:8px 16px;font-size:13px}section{padding:56px 18px}.cta-strip{padding:56px 18px}.cta-section{padding:56px 18px}.hero{padding:96px 16px 56px;min-height:auto}h1{font-size:clamp(40px,9vw,64px)}.s-title{font-size:clamp(30px,6vw,48px)!important}.stat-row{gap:20px;margin-top:44px}.stat+.stat::before{display:none}.stat-num{font-size:32px}.proof-card{padding:24px;margin-top:32px}.proof-quote{font-size:15px}.proof-result{position:static;display:inline-block;margin-bottom:14px}.cta-section h2{font-size:clamp(30px,6vw,46px)}.cta-strip h2{font-size:clamp(28px,6vw,46px)}.smet{min-width:100px;padding:12px 14px}.smet-v{font-size:24px}}
|
||||
@media(max-width:480px){body{font-size:15px}nav{padding:10px 14px}.logo-text{font-size:16px}.nav-cta{padding:7px 14px;font-size:12px}section{padding:48px 16px}.cta-strip{padding:48px 16px}.cta-section{padding:48px 16px}.hero{padding:88px 14px 48px}h1{font-size:clamp(36px,11vw,56px);max-width:100%}.hero-sub{font-size:15px}.s-title{font-size:clamp(26px,7vw,42px)!important}.s-sub{font-size:15px}.stat-row{gap:18px;margin-top:36px}.stat-num{font-size:28px}.proof-card{padding:20px}.proof-quote{font-size:14.5px;line-height:1.7}.cta-section h2{font-size:clamp(26px,8vw,40px)}.cta-strip h2{font-size:clamp(26px,8vw,40px)}.btn-primary,.btn-ghost{padding:12px 22px;font-size:15px}.pain-grid{margin-top:32px}.p-item{padding:22px 18px}.smet-row{gap:8px}.smet{min-width:calc(50% - 4px);padding:10px 12px}.mob-sticky{padding:10px 12px;gap:8px}.mob-sticky a{padding:10px;font-size:13px}.cta-micro{flex-direction:column;gap:6px}.strip-trust{flex-direction:column;gap:8px}.micro-row{gap:10px}.hero-actions{flex-direction:column;width:100%}.hero-actions .btn-primary,.hero-actions .btn-ghost{width:100%;text-align:center}.strip-btns{flex-direction:column;width:100%}.strip-btns .btn-primary,.strip-btns .btn-ghost{width:100%;text-align:center}.cta-form .btn-primary{width:100%;text-align:center}}
|
||||
@media(max-width:380px){h1{font-size:clamp(30px,12vw,48px)}.s-title{font-size:clamp(24px,8vw,38px)!important}.stat-num{font-size:24px}.sol-stats{grid-template-columns:1fr}.smet{min-width:100%}}
|
||||
/* ── bg helpers (proven v3) ── */
|
||||
.bg-img{position:absolute!important;inset:0!important;z-index:0!important;background-size:cover!important;background-position:center!important;pointer-events:none}
|
||||
.bg-ov{position:absolute!important;inset:0!important;z-index:1!important;pointer-events:none}
|
||||
.has-bg{position:relative!important;overflow:hidden!important}
|
||||
.has-bg>.container,.has-bg>.hero-bg,.has-bg>.eyebrow,
|
||||
.has-bg>h1,.has-bg>.hero-sub,.has-bg>.hero-actions,
|
||||
.has-bg>.micro-row,.has-bg>.stat-row,.has-bg>.wrap{position:relative!important;z-index:2!important}
|
||||
/* ── CTA strip ── */
|
||||
.cta-strip{padding:84px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip>.wrap{position:relative;z-index:2;max-width:700px;margin:0 auto}
|
||||
.strip-tag{display:inline-flex;align-items:center;gap:7px;padding:5px 16px;border-radius:100px;margin-bottom:22px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.strip-tdot{width:6px;height:6px;border-radius:50%;display:inline-block;animation:blink 2s ease infinite}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5vw,60px);font-weight:900;line-height:.97;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.72;max-width:520px;margin:0 auto 28px}
|
||||
.strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.strip-trust{display:flex;gap:18px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.strip-trust b{color:var(--green)}
|
||||
.smet-row{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:28px}
|
||||
.smet{background:var(--card);border-radius:12px;padding:14px 20px;text-align:center;min-width:118px}
|
||||
.smet-v{font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;line-height:1}
|
||||
.smet-l{font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.4px;line-height:1.4}
|
||||
/* ── bigger s-title ── */
|
||||
.s-title{font-size:clamp(36px,5vw,62px)!important;line-height:.97!important}
|
||||
|
||||
</style>
|
||||
<title>LynkedUp Pro - Replace 5 Roofing Tools With One $349/mo Platform</title>
|
||||
<meta name="description" content="Replace EagleView, JobNimbus, SalesRabbit, and more. One flat $349/mo. No per-user fees. No per-report charges. All 6 core systems built for roofing."/>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a>
|
||||
<a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a>
|
||||
</nav>
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=1800&q=80');filter:brightness(.11) saturate(.4)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(5,8,15,.45) 0%,rgba(5,8,15,.8) 55%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(0,229,160,.08),transparent 65%)"></div></div>
|
||||
<div class="eyebrow"><span class="dot" style="background:var(--green)"></span> All-In-One Platform · Flat $349/mo · 6 Core Systems</div>
|
||||
<h1>Count Your<br><span style="color:var(--red)">Subscriptions.</span><br><span style="color:var(--green)">Now Cut Them.</span></h1>
|
||||
<p class="hero-sub">EagleView. JobNimbus. SalesRabbit. A billing tool. A proposal tool. <strong>None of them talk to each other.</strong> LynkedUp Pro replaces all of them for $349/mo flat, and they all work together. It's your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Book a demo</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro" style="color:var(--muted)"><span style="color:var(--orange)">4.9★</span> · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>✅</span> 6 core systems, one login</span>
|
||||
<span class="micro"><span>✅</span> No per-user fees</span>
|
||||
<span class="micro"><span>✅</span> No per-report charges</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">$349</div><div class="stat-label">Flat rate - everything included</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">6</div><div class="stat-label">Core systems in one platform</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">$800+</div><div class="stat-label">Avg monthly savings vs. stack</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">1</div><div class="stat-label">Login. One source of truth.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(11,18,32,.98) 0%,rgba(11,18,32,.93) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Real Cost</div>
|
||||
<h2 class="s-title">$1,300 a Month.<br>5 Logins. Zero<br><span style="color:var(--red)">Working Together.</span></h2>
|
||||
<p class="s-sub">You're not just overpaying. You're also wasting hours every week copying data between tools that were never designed to work together.</p>
|
||||
<div style="background:var(--card);border:1px solid var(--border);border-radius:14px;padding:28px;margin-top:40px">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--red);text-transform:uppercase;letter-spacing:1px;margin-bottom:20px">What most Texas roofing crews pay right now</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:20px">
|
||||
<div style="background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.15);border-radius:10px;padding:16px;text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--red)">$700+</div><div style="font-size:12px;color:var(--muted);margin-top:4px">EagleView reports/mo</div></div>
|
||||
<div style="background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.15);border-radius:10px;padding:16px;text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--red)">$300+</div><div style="font-size:12px;color:var(--muted);margin-top:4px">JobNimbus or AccuLynx</div></div>
|
||||
<div style="background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.15);border-radius:10px;padding:16px;text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--red)">$200+</div><div style="font-size:12px;color:var(--muted);margin-top:4px">SalesRabbit canvassing</div></div>
|
||||
<div style="background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.15);border-radius:10px;padding:16px;text-align:center"><div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--red)">$100+</div><div style="font-size:12px;color:var(--muted);margin-top:4px">Billing + proposal tools</div></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-top:1px solid var(--border);flex-wrap:wrap;gap:12px">
|
||||
<div><div style="font-family:'Barlow Condensed',sans-serif;font-size:36px;font-weight:900;color:var(--red)">$1,300+</div><div style="font-size:13px;color:var(--muted)">Total monthly tool stack</div></div>
|
||||
<div style="font-size:24px;color:var(--muted)">→</div>
|
||||
<div style="text-align:right"><div style="font-family:'Barlow Condensed',sans-serif;font-size:36px;font-weight:900;color:var(--green)">$349</div><div style="font-size:13px;color:var(--muted)">LynkedUp Pro - all-in</div></div>
|
||||
<div style="text-align:right"><div style="font-family:'Barlow Condensed',sans-serif;font-size:36px;font-weight:900;color:var(--orange)">$951</div><div style="font-size:13px;color:var(--muted)">Saved every month</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pain-grid" style="margin-top:16px">
|
||||
<div class="p-item"><div class="p-icon">🔀</div><h3>Data Lives in 5 Places</h3><p>Lead is in JobNimbus. Measurement is in EagleView. Route is in SalesRabbit. Invoice is in QuickBooks. Nothing syncs. Your team copies and pastes all day.</p><span class="p-cost">Cost: 2+ hours/day wasted</span></div>
|
||||
<div class="p-item"><div class="p-icon">💳</div><h3>Hidden Fees Everywhere</h3><p>EagleView per report. JobNimbus per user. SalesRabbit per user. AccuLynx add-ons. Your "software budget" doubles every time you add a rep.</p><span class="p-cost">Cost: Unpredictable monthly bill</span></div>
|
||||
<div class="p-item"><div class="p-icon">🧩</div><h3>None of It Was Built for Roofing</h3><p>JobNimbus serves landscapers, plumbers, HVAC. SalesRabbit serves solar and insurance. You're adapting general tools to a specialized trade. That's the real problem.</p><span class="p-cost">Cost: Constant workarounds</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══ CTA STRIP ══ -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1800&q=80');filter:brightness(.09) saturate(.38)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(2,10,5,.97) 100%)"></div>
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:360px;border-radius:50%;background:radial-gradient(ellipse,rgba(239,68,68,.06),transparent 70%);z-index:1;pointer-events:none"></div>
|
||||
<div class="wrap">
|
||||
<div class="strip-tag" style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.28);color:var(--red)">
|
||||
<span class="strip-tdot" style="background:var(--red)"></span>
|
||||
Your tool stack bill arrives again next month
|
||||
</div>
|
||||
<h2>$951 Back<br>in Your Pocket.<br><span style="color:var(--green)">Every. Single. Month.</span></h2>
|
||||
<p>The math is simple. You're paying over $1,300/mo for tools that were never designed to work together. LynkedUp Pro does all of it for $349 flat, and the data flows between every module automatically.</p>
|
||||
<div class="smet-row"><div class="smet" style="border:1px solid rgba(239,68,68,.22)"><div class="smet-v" style="color:var(--red)">$1,300+</div><div class="smet-l">Current tool stack /mo</div></div><div class="smet" style="border:1px solid rgba(0,229,160,.22)"><div class="smet-v" style="color:var(--green)">$349</div><div class="smet-l">LynkedUp Pro flat rate</div></div><div class="smet" style="border:1px solid rgba(245,158,11,.22)"><div class="smet-v" style="color:var(--orange)">$951</div><div class="smet-l">Monthly savings</div></div><div class="smet" style="border:1px solid rgba(0,200,255,.22)"><div class="smet-v" style="color:var(--accent)">$11.4K</div><div class="smet-l">Annual back in pocket</div></div></div>
|
||||
<div class="strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Show Me My Savings →</a>
|
||||
|
||||
</div>
|
||||
<div class="strip-trust">
|
||||
<span><b>✅</b> No credit card</span>
|
||||
<span><b>✅</b> Free 30-min demo</span>
|
||||
<span><b>✅</b> 30-day guarantee</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(90deg,rgba(5,8,15,1) 42%,rgba(5,8,15,.75) 100%)"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div class="s-label" style="color:var(--green)">The Fix</div>
|
||||
<h2 class="s-title">Cancel 4 Bills.<br><span style="color:var(--green)">Keep One.</span><br>Run It All Better.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">Every tool your roofing business needs - built to work together from day one. One login, one source of truth, one flat price that doesn't change when you hire your next rep.</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:20px">
|
||||
<div style="background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">👥</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">CRM</div><div style="font-size:12px;color:var(--muted)">Pipeline, leads, AI insights</div></div>
|
||||
<div style="background:var(--card);border:1px solid rgba(245,158,11,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">🎨</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">Canvas</div><div style="font-size:12px;color:var(--muted)">Estimates, proposals, e-sign</div></div>
|
||||
<div style="background:var(--card);border:1px solid rgba(0,229,160,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">📐</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">Measurements</div><div style="font-size:12px;color:var(--muted)">AI + LiDAR, $0/report</div></div>
|
||||
<div style="background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">⭐</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">Client Experience</div><div style="font-size:12px;color:var(--muted)">Portals, reviews, follow-ups</div></div>
|
||||
<div style="background:var(--card);border:1px solid rgba(245,158,11,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">💳</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">Billing</div><div style="font-size:12px;color:var(--muted)">Invoices, payments, cash flow</div></div>
|
||||
<div style="background:var(--card);border:1px solid rgba(0,229,160,.15);border-radius:10px;padding:16px"><div style="font-size:18px;margin-bottom:8px">🧮</div><div style="font-family:'Barlow Condensed',sans-serif;font-size:15px;font-weight:700;color:#fff;margin-bottom:4px;text-transform:uppercase">Estimating</div><div style="font-size:12px;color:var(--muted)">Auto pricing, templates</div></div>
|
||||
</div>
|
||||
<div style="margin-top:16px;background:rgba(0,229,160,.04);border:1px solid rgba(0,229,160,.18);border-radius:10px;padding:16px;font-size:14px;color:var(--muted);line-height:1.65">
|
||||
<strong style="color:var(--green)">Plus hail alerts, smart calendar, adjuster reports, LiDAR scanning, and GPS rep tracking</strong>. All included. No add-ons. No surprises. No per-user math.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-card">
|
||||
<img src="/page/assets/overview13 (1).png" alt="LynkedUp Pro Overview Dashboard" style="display:block;width:100%;height:auto" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="proof-section has-bg reveal">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1503387762-592deb58ef4e?w=1800&q=80');filter:brightness(.09) saturate(.35)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.88) 60%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--green);text-align:center">Real Result</div>
|
||||
<h2 class="s-title" style="text-align:center">He Cut Three Tools<br>on a Tuesday. <span style="color:var(--green)">Saved<br>$2,100 by Friday.</span></h2>
|
||||
<div class="proof-card">
|
||||
<div class="proof-result"><div class="pr-num">$2.1K</div><div class="pr-label">Saved/Month</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"Cut EagleView, JobNimbus, and SalesRabbit in one move. LynkedUp Pro replaced all three for less, and the data actually flows between them. My team doesn't copy and paste anything anymore. It's a completely different way of running the business."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MT</div>
|
||||
<div><div class="proof-name">Marcus Thompson</div><div class="proof-role">Operations Manager · Houston, TX</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-ov" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(245,158,11,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:1">
|
||||
<div style="display:inline-block;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--green);font-weight:600">💰 Most crews save $800-$1,100/mo by switching to LynkedUp Pro</div>
|
||||
<h2>Your Stack Bill<br>Is Due Next Month.<br><em>Make It the Last One.</em></h2>
|
||||
<p>Cancel your EagleView, JobNimbus, and SalesRabbit subscriptions. Book a 30-minute demo and we'll show you exactly how.</p>
|
||||
<div class="cta-form">
|
||||
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary" onclick="event.preventDefault()">Book a demo</a>
|
||||
</div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day money-back guarantee</span><span>✅ Free onboarding + data migration help</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<p>© 2026 LynkedUp Pro · <a href="lynkeduppro_final.html">See Full Platform →</a> · <a href="https://book.lynkeduppro.com/">Privacy</a> · <a href="https://book.lynkeduppro.com/">Terms</a></p>
|
||||
<p style="margin-top:8px;font-size:12px;opacity:.5">$349/mo flat rate · No credit card required · 30-day money-back guarantee · Built for roofing</p>
|
||||
<p style="margin-top:8px;font-size:11px;opacity:.5">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p>
|
||||
</footer>
|
||||
<div class="mob-sticky">
|
||||
<a href="https://book.lynkeduppro.com/" class="p">Book a demo</a>
|
||||
<a href="lynkeduppro_final.html" class="s">See Full Platform</a>
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add("visible")})},{threshold:.1});
|
||||
document.querySelectorAll(".reveal").forEach(el=>obs.observe(el));
|
||||
document.querySelectorAll("a[href^=\"#\"]").forEach(a=>{a.addEventListener("click",function(e){const t=document.querySelector(this.getAttribute("href"));if(t){e.preventDefault();t.scrollIntoView({behavior:"smooth",block:"start"});}}); });
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,481 @@
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>LynkedUp Pro - Know Your Conversion Rate. Know Who's Performing.</title>
|
||||
<meta name="description" content="Track verified leads, unverified leads, conversion rate, agent revenue volume, and deal count. Know who's performing and where every lead stands."/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
.shot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border);
|
||||
object-fit: cover;
|
||||
box-shadow: 0 36px 72px rgba(0, 0, 0, .5);
|
||||
}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
@media(max-width:1280px){section{padding:80px 24px}nav{padding:16px 32px}}
|
||||
@media(max-width:1024px){section{padding:72px 24px}.pain-grid{grid-template-columns:repeat(2,1fr)}.sol-split{grid-template-columns:1fr;gap:40px}h1{font-size:clamp(42px,7vw,72px)}.s-title{font-size:clamp(30px,4.5vw,48px)}.cta-section h2{font-size:clamp(32px,5vw,52px)}}
|
||||
@media(max-width:860px){nav{padding:14px 20px}.sol-split{grid-template-columns:1fr!important;gap:36px}.pain-grid{grid-template-columns:1fr!important}.sol-stats{grid-template-columns:1fr 1fr}.mob-sticky{display:flex}body{padding-bottom:72px}section{padding:64px 20px}.hero{padding:100px 16px 60px;min-height:auto}.hero-actions{flex-direction:column;width:100%;max-width:320px;margin-left:auto;margin-right:auto}.hero-actions .btn-primary,.hero-actions .btn-ghost{text-align:center;width:100%}.stat-row{gap:28px}.stat-num{font-size:32px}.cta-section{padding:64px 20px}.proof-card{padding:28px 22px}.proof-result{position:static;display:inline-block;margin:0 0 14px}}
|
||||
@media(max-width:560px){.stat-row{gap:20px;flex-direction:column}.stat+.stat::before{display:none}h1{font-size:clamp(36px,11vw,64px)}.s-title{font-size:clamp(26px,7.5vw,40px)}.cta-section h2{font-size:clamp(28px,8vw,44px)}.cta-form{flex-direction:column}.cta-form input{min-width:100%;width:100%}.cta-form .btn-primary{width:100%;text-align:center}.sol-stats{grid-template-columns:1fr}.hero-sub{font-size:15px}.micro-row{gap:10px}section{padding:56px 16px}.hero{padding:96px 16px 48px}.eyebrow{font-size:11px;padding:5px 12px}.proof-quote{font-size:15px}.cta-section p{font-size:15px}}
|
||||
@media(max-width:380px){nav{padding:12px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}h1{font-size:clamp(32px,11vw,48px)}.s-title{font-size:24px}.btn-primary,.btn-ghost{padding:12px 22px;font-size:14px}.cta-section h2{font-size:28px}.proof-card{padding:22px 18px}}</style></head><body>
|
||||
<nav><a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a><a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a></nav>
|
||||
|
||||
<section class="hero" id="home">
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(0,200,255,.09),transparent 65%)"></div></div>
|
||||
<div class="eyebrow"><span class="dot"></span> Pipeline Metrics · Conversion Tracking · Agent Performance</div>
|
||||
<h1>You Don't Know<br>Your Conversion Rate.<br><span style="color:var(--accent)">That's the Problem.</span></h1>
|
||||
<p class="hero-sub"><strong>How many leads are verified? How many converted?</strong> Which rep closes and which one stalls? Without metrics you're managing on vibes, and losing revenue you don't even know you're losing. LynkedUp Pro is your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">See Pipeline Metrics →</a>
|
||||
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro" style="color:var(--muted)">4.9★ · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>✅</span> Verified vs unverified leads</span>
|
||||
<span class="micro"><span>✅</span> Live conversion rate</span>
|
||||
<span class="micro"><span>✅</span> Per-agent revenue tracking</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">4</div><div class="stat-label">Lead status categories</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">Live</div><div class="stat-label">Conversion rate dashboard</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">Per</div><div class="stat-label">Agent revenue & deal count</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">Role</div><div class="stat-label">Based access control</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pain-section reveal" id="problem">
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">Your Pipeline Is Full.<br><span style="color:var(--red)">But You're Still Missing Numbers.</span></h2>
|
||||
<p class="s-sub">A big pipeline looks great. But if you don't know your conversion rate, you can't fix it. If you don't track per-agent volume, your best rep is carrying your weakest one.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item"><div class="p-icon">❓</div><h3>No Lead Verification</h3><p>Is this a real homeowner ready to buy, or a tire-kicker your rep spent 3 hours on? Unverified leads pollute your pipeline and inflate your numbers.</p><span class="p-cost">Cost: Wasted rep hours daily</span></div>
|
||||
<div class="p-item"><div class="p-icon">📉</div><h3>Conversion Rate Invisible</h3><p>You know how many leads came in. You know how many jobs closed. But no one has calculated the rate in between, and that's where all the coaching opportunity lives.</p><span class="p-cost">Cost: Uncoached underperformers</span></div>
|
||||
<div class="p-item"><div class="p-icon">👤</div><h3>No Per-Agent Visibility</h3><p>One rep closes 70% of their deals. Another closes 20%. Both look the same on the surface because nobody's tracking deal count and revenue volume per person.</p><span class="p-cost">Cost: Wrong reps in wrong territories</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== CTA STRIP 1 ===== -->
|
||||
<section style="padding:72px 24px;background:linear-gradient(135deg,#080e1a 0%,#0a1220 50%,#06090f 100%);border-top:1px solid rgba(239,68,68,.12);border-bottom:1px solid rgba(239,68,68,.12);text-align:center;position:relative;overflow:hidden">
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:600px;height:300px;background:radial-gradient(ellipse,rgba(239,68,68,.05),transparent 70%);pointer-events:none"></div>
|
||||
<div style="max-width:600px;margin:0 auto;position:relative;z-index:1">
|
||||
<div style="display:inline-flex;align-items:center;gap:7px;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:100px;padding:5px 14px;margin-bottom:20px">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:var(--red);display:inline-block;animation:blink 2s ease infinite"></span>
|
||||
<span style="font-size:11px;font-weight:600;color:var(--red);letter-spacing:1px;text-transform:uppercase">You're Bleeding Revenue Right Now</span>
|
||||
</div>
|
||||
<h2 style="font-family:'Barlow Condensed',sans-serif;font-size:clamp(32px,4.5vw,52px);font-weight:900;color:#fff;line-height:1.05;text-transform:uppercase;letter-spacing:-.5px;margin-bottom:14px">You Can't Fix<br><span style="color:var(--red)">What You Can't See.</span></h2>
|
||||
<p style="font-size:16px;color:var(--muted);line-height:1.7;max-width:460px;margin:0 auto 28px">Right now, one of your reps is closing 20% while another closes 70%. Same leads. Same territory. You just don't know who. <strong style="color:var(--text)">Let's fix that in 30 minutes.</strong></p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
||||
|
||||
<a href="#solution" class="btn-ghost">See How It Works</a>
|
||||
</div>
|
||||
<div style="display:flex;gap:20px;justify-content:center;margin-top:16px;flex-wrap:wrap">
|
||||
<span style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span> Free 30-min demo</span>
|
||||
<span style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span> No credit card required</span>
|
||||
<span style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span> Live pipeline data</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section reveal visible" id="solution">
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--accent)">The Fix</div>
|
||||
<h2 class="s-title">Every Metric<br>That <span style="color:var(--accent)">Actually</span><br>Matters.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro tracks every lead through a verified status pipeline and gives every manager and owner live conversion data, per-agent breakdowns, and deal volume, all in one dashboard.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">✅</span><span><strong>4 lead status categories</strong>: Verified, Unverified, Converted, and Lost. Every lead correctly categorized from first contact.</span></li>
|
||||
<li><span class="ico">📊</span><span><strong>Live conversion rate</strong>: per team, per rep, per territory. Updated in real time as leads move through the pipeline.</span></li>
|
||||
<li><span class="ico">💰</span><span><strong>Per-agent revenue volume</strong>: total revenue attributed to each rep, plus deal count, close rate, and average deal size.</span></li>
|
||||
<li><span class="ico">🔐</span><span><strong>Role-based access</strong>: reps see their own numbers, managers see their team, admins assign leads, owners see everything. No data leakage.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">Real-Time</div><div class="sol-stat-label">Conversion rate tracking</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">4</div><div class="sol-stat-label">Lead verification statuses</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">Per Rep</div><div class="sol-stat-label">Revenue & deal count</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:#fff">Admin</div><div class="sol-stat-label">Lead assignment controls</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PIPELINE METRICS DASHBOARD SCREENSHOT -->
|
||||
<img src="/page/assets/dashboard.png" alt="LynkedUp Pro pipeline metrics and agent performance dashboard" class="shot" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<!-- ===== CTA STRIP 2 ===== -->
|
||||
<section style="padding:80px 24px;background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border);text-align:center;position:relative;overflow:hidden">
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:400px;background:radial-gradient(ellipse,rgba(0,200,255,.05),transparent 70%);pointer-events:none"></div>
|
||||
<div style="max-width:680px;margin:0 auto;position:relative;z-index:1">
|
||||
<div style="display:inline-block;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);border-radius:8px;padding:7px 18px;margin-bottom:22px;font-size:12px;color:var(--accent);font-weight:600;letter-spacing:.3px;text-transform:uppercase">📊 Live for 500+ Roofing Companies</div>
|
||||
<h2 style="font-family:'Barlow Condensed',sans-serif;font-size:clamp(32px,4.5vw,54px);font-weight:900;color:#fff;line-height:1;text-transform:uppercase;letter-spacing:-.5px;margin-bottom:14px">Stop Managing<br>on <span style="color:var(--orange)">Gut Feeling.</span><br><span style="color:var(--accent)">Start With Real Numbers.</span></h2>
|
||||
<p style="font-size:16px;color:var(--muted);line-height:1.7;max-width:480px;margin:0 auto 32px">The dashboard is live in 24 hours. You'll see your conversion rate, your verified vs unverified lead split, and every rep's numbers, before your next morning huddle.</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:32px">
|
||||
<div style="background:var(--card);border:1px solid rgba(0,200,255,.18);border-radius:10px;padding:14px 20px;min-width:130px;text-align:center">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;color:var(--accent);line-height:1">38.2%</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">Conversion Rate</div>
|
||||
</div>
|
||||
<div style="background:var(--card);border:1px solid rgba(0,229,160,.18);border-radius:10px;padding:14px 20px;min-width:130px;text-align:center">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;color:var(--green);line-height:1">89</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">Verified Leads</div>
|
||||
</div>
|
||||
<div style="background:var(--card);border:1px solid rgba(245,158,11,.18);border-radius:10px;padding:14px 20px;min-width:130px;text-align:center">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;color:var(--orange);line-height:1">$9K</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">Top Rep Revenue</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Get the Dashboard Free →</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="proof-section reveal">
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--accent);text-align:center">Real Impact</div>
|
||||
<h2 class="s-title" style="text-align:center">Found the Problem.<br><span style="color:var(--accent)">Fixed the Team. Fast.</span></h2>
|
||||
<div class="proof-card">
|
||||
<div class="proof-result"><div class="pr-num">+43%</div><div class="pr-label">Close Rate</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"When I saw the agent breakdown for the first time, I realized one rep had an 18% close rate while another had 68%. Same leads, same territory. I moved assignments around and the lower performer jumped to 34% in six weeks. I would never have known without the data."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#0ea5e9,#00e5a0)">JR</div>
|
||||
<div><div class="proof-name">Jason Rodriguez</div><div class="proof-role">Owner · Dallas, TX · 14-person crew</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta-section" id="cta">
|
||||
<div class="container" style="position:relative;z-index:1">
|
||||
<div style="display:inline-block;background:rgba(0,200,255,.08);border:1px solid rgba(0,200,255,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--accent);font-weight:600">📊 Know your conversion rate before your next storm season</div>
|
||||
<h2>Know Who's Performing.<br><em>Fix What Isn't.</em></h2>
|
||||
<p>See the full pipeline metrics dashboard, with verified leads, conversion rate, and per-agent revenue, in a free 30-minute demo.</p>
|
||||
<div class="cta-form"><a href="https://book.lynkeduppro.com/" class="btn-primary" onclick="event.preventDefault()">See Metrics Dashboard →</a></div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day guarantee</span><span>✅ Free onboarding</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer style="padding:32px 24px;border-top:1px solid var(--border);text-align:center"><p style="font-size:13px;color:var(--muted)">© 2026 LynkedUp Pro Full Platform →</p><p style="font-size:11px;color:var(--muted);opacity:.7;margin-top:10px">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p></footer>
|
||||
<div class="mob-sticky"><a href="https://book.lynkeduppro.com/" class="p">Book a demo</a><a href="lynkeduppro_final.html" class="s">See All Features</a></div>
|
||||
<script>const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add('visible')})},{threshold:.1});document.querySelectorAll('.reveal').forEach(el=>obs.observe(el));</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,470 @@
|
||||
<!DOCTYPE html><html lang="en"><head>
|
||||
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>LynkedUp Pro - Everything You Need in 60 Seconds</title>
|
||||
<meta name="description" content="Replace EagleView, JobNimbus & SalesRabbit. AI measurements. Hail alerts. Smart calendar. Flat 49/mo. 30-day guarantee."/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
@media(max-width:860px){nav{padding:14px 20px}.sol-split{grid-template-columns:1fr!important}.pain-grid{grid-template-columns:1fr!important}.sol-stats{grid-template-columns:1fr 1fr}.mob-sticky{display:flex}body{padding-bottom:72px}section{padding:64px 20px}.hero{padding:100px 16px 60px}}
|
||||
@media(max-width:560px){.stat-row{gap:24px}h1{font-size:clamp(44px,13vw,80px)}.cta-form{flex-direction:column}.cta-form input{min-width:100%}}
|
||||
.quick-hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:80px 24px;position:relative;overflow:hidden}
|
||||
.quick-bg{inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black,transparent)}
|
||||
.glow-c{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:1000px;height:700px;background:radial-gradient(ellipse,rgba(0,200,255,.07),transparent 65%);pointer-events:none}
|
||||
.q-eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:28px;letter-spacing:.3px}
|
||||
.q-dot{width:6px;height:6px;border-radius:50%;background:var(--green);animation:blink 1.5s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
.q-h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(52px,9vw,110px);font-weight:900;line-height:.9;letter-spacing:-2px;color:#fff;text-transform:uppercase;max-width:900px}
|
||||
.q-h1 .blue{color:var(--accent)}.q-h1 .orange{color:var(--orange)}
|
||||
.q-sub{font-size:clamp(16px,2vw,20px);color:var(--muted);max-width:560px;margin:20px auto 0;line-height:1.6}
|
||||
.q-sub strong{color:#fff}
|
||||
.q-actions{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.q-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;max-width:900px;margin:60px auto 0}
|
||||
.q-stat{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:24px 16px;text-align:center;transition:border-color .25s}
|
||||
.q-stat:hover{border-color:var(--border2)}
|
||||
.q-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:42px;font-weight:900;line-height:1;margin-bottom:6px}
|
||||
.q-stat-label{font-size:13px;color:var(--muted);line-height:1.4}
|
||||
.q-strip{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border);padding:24px;margin-top:0}
|
||||
.q-strip-inner{max-width:900px;margin:0 auto;display:flex;gap:0;flex-wrap:wrap}
|
||||
.q-strip-item{flex:1;min-width:160px;padding:0 20px;text-align:center;border-right:1px solid var(--border)}
|
||||
.q-strip-item:last-child{border-right:none}
|
||||
.q-strip-icon{font-size:24px;margin-bottom:6px}
|
||||
.q-strip-title{font-family:'Barlow Condensed',sans-serif;font-size:14px;font-weight:700;color:#fff;text-transform:uppercase;margin-bottom:4px}
|
||||
.q-strip-desc{font-size:12px;color:var(--muted);line-height:1.5}
|
||||
.q-vs{background:var(--bg);padding:48px 24px}
|
||||
.q-vs-inner{max-width:860px;margin:0 auto;display:grid;grid-template-columns:1fr auto 1fr;gap:24px;align-items:center}
|
||||
.q-vs-col{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:24px}
|
||||
.q-vs-col.win{border-color:rgba(0,229,160,.3);background:linear-gradient(160deg,rgba(0,229,160,.04),var(--card))}
|
||||
.q-vs-label{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:14px}
|
||||
.q-vs-items{display:flex;flex-direction:column;gap:10px}
|
||||
.q-vs-item{display:flex;align-items:center;gap:8px;font-size:14px;color:var(--muted)}
|
||||
.q-vs-item .ico{font-size:14px;min-width:20px;text-align:center}
|
||||
.q-vs-sep{font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--muted);text-align:center}
|
||||
.q-cta{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:72px 24px;text-align:center}
|
||||
.q-price{font-family:'Barlow Condensed',sans-serif;font-size:clamp(60px,8vw,100px);font-weight:900;color:var(--orange);line-height:1;letter-spacing:-2px}
|
||||
.q-price sub{font-size:24px;font-weight:500;color:var(--muted);letter-spacing:0}
|
||||
.q-price-note{font-size:17px;color:var(--muted);margin:10px 0 32px}
|
||||
@media(max-width:680px){.q-stats{grid-template-columns:1fr 1fr}.q-strip-item{min-width:50%;border-bottom:1px solid var(--border);border-right:none;padding:12px 16px}.q-vs-inner{grid-template-columns:1fr}.q-vs-sep{display:none}}
|
||||
@media(max-width:1280px){section{padding:80px 24px}nav{padding:16px 32px}.q-cta{padding:64px 24px}.q-vs{padding:40px 24px}}
|
||||
@media(max-width:1024px){section{padding:72px 24px}.q-stats{grid-template-columns:repeat(2,1fr);max-width:600px}.q-strip-inner{flex-wrap:wrap}.q-strip-item{flex:1 1 33.33%;min-width:33.33%;border-bottom:1px solid var(--border)}.q-h1{font-size:clamp(46px,8vw,84px)}.q-vs-inner{gap:18px}}
|
||||
@media(max-width:860px){nav{padding:14px 20px}.sol-split{grid-template-columns:1fr!important;gap:36px}.pain-grid{grid-template-columns:1fr!important}.sol-stats{grid-template-columns:1fr 1fr}.mob-sticky{display:flex}body{padding-bottom:72px}section{padding:64px 20px}.hero{padding:100px 16px 60px}.quick-hero{padding:96px 20px 56px;min-height:auto}.q-actions{flex-direction:column;width:100%;max-width:320px;margin-left:auto;margin-right:auto}.q-actions .btn-primary,.q-actions .btn-ghost{text-align:center;width:100%}.q-h1{font-size:clamp(40px,9vw,72px)}.q-strip{padding:20px 16px}.q-strip-inner{gap:0}.q-vs{padding:48px 20px}.q-vs-col{padding:20px}.q-cta{padding:56px 20px}}
|
||||
@media(max-width:560px){.stat-row{gap:24px}h1{font-size:clamp(44px,13vw,80px)}.cta-form{flex-direction:column}.cta-form input{min-width:100%}.q-h1{font-size:clamp(36px,11vw,60px);letter-spacing:-1px}.q-sub{font-size:15px}.q-stats{grid-template-columns:1fr 1fr;gap:10px}.q-stat{padding:18px 12px}.q-stat-num{font-size:32px}.q-strip-item{flex:1 1 50%;min-width:50%}.q-vs-col{padding:18px}.q-vs-item{font-size:13px}.q-price{font-size:clamp(48px,14vw,72px)}.q-price-note{font-size:15px}section{padding:56px 16px}.eyebrow,.q-eyebrow{font-size:11px;padding:5px 12px}}
|
||||
@media(max-width:380px){nav{padding:12px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}.q-h1{font-size:clamp(32px,11vw,52px)}.q-stats{grid-template-columns:1fr}.q-strip-item{flex:1 1 100%;min-width:100%}.btn-primary,.btn-ghost{padding:12px 22px;font-size:14px}.q-actions .btn-primary{font-size:16px!important;padding:14px 28px!important}}
|
||||
</style></head><body>
|
||||
<nav><a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a><a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a></nav>
|
||||
|
||||
<section class="quick-hero" id="home">
|
||||
<div class="quick-bg"></div><div class="glow-c" aria-hidden="true"></div>
|
||||
<div class="q-eyebrow"><span class="q-dot"></span> Built for Roofing · Flat 49/mo · 30-Day Guarantee</div>
|
||||
<h1 class="q-h1">One Tool.<br><span class="blue">Every Job.</span><br><span class="orange">Done.</span></h1>
|
||||
<p class="q-sub">Replace <strong>EagleView, JobNimbus & SalesRabbit</strong> with one AI-powered roofing platform. Hail alerts. AI measurements. Smart calendar. All in. It's your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="q-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary" style="font-size:18px;padding:16px 40px">Book a demo →</a>
|
||||
<a href="lynkeduppro_final.html" class="btn-ghost" style="font-size:16px;padding:16px 28px">See All Features</a>
|
||||
</div>
|
||||
<div style="font-size:14px;color:var(--muted);margin-top:16px">4.9★ · 248 reviews</div>
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:16px;margin-top:18px;flex-wrap:wrap">
|
||||
<span style="font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span>No credit card</span>
|
||||
<span style="font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span>30-day guarantee</span>
|
||||
<span style="font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px"><span style="color:var(--green)">✅</span>Cancel anytime</span>
|
||||
</div>
|
||||
<div class="q-stats">
|
||||
<div class="q-stat"><div class="q-stat-num" style="color:var(--orange)">$0</div><div class="q-stat-label">Per-report fee. Ever.</div></div>
|
||||
<div class="q-stat"><div class="q-stat-num" style="color:var(--accent)">17 min</div><div class="q-stat-label">Storm alert to reps on doors</div></div>
|
||||
<div class="q-stat"><div class="q-stat-num" style="color:var(--green)">+43%</div><div class="q-stat-label">Close rate lift (beta crews)</div></div>
|
||||
<div class="q-stat"><div class="q-stat-num" style="color:#fff">49</div><div class="q-stat-label">All 6 systems. Flat rate.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="q-strip">
|
||||
<div class="q-strip-inner">
|
||||
<div class="q-strip-item"><div class="q-strip-icon">📅</div><div class="q-strip-title">Smart Calendar</div><div class="q-strip-desc">AI scheduling + iOS/Google/Calendly sync</div></div>
|
||||
<div class="q-strip-item"><div class="q-strip-icon">📐</div><div class="q-strip-title">AI Measurements</div><div class="q-strip-desc">LiDAR scan in minutes. $0 per report.</div></div>
|
||||
<div class="q-strip-item"><div class="q-strip-icon">⚡</div><div class="q-strip-title">Hail Alerts</div><div class="q-strip-desc">Sub-hour storm alerts + address scoring</div></div>
|
||||
<div class="q-strip-item"><div class="q-strip-icon">📋</div><div class="q-strip-title">Adjuster Reports</div><div class="q-strip-desc">1-click. Insurance-accepted. Auto-generated.</div></div>
|
||||
<div class="q-strip-item"><div class="q-strip-icon">📊</div><div class="q-strip-title">Pipeline CRM</div><div class="q-strip-desc">Leads, stages, estimates, proposals</div></div>
|
||||
<div class="q-strip-item"><div class="q-strip-icon">🏠</div><div class="q-strip-title">Owner's Box</div><div class="q-strip-desc">Revenue, KPIs, team performance, live</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-vs">
|
||||
<div style="text-align:center;margin-bottom:32px">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:clamp(28px,4vw,44px);font-weight:900;color:#fff;text-transform:uppercase">Your Current Stack vs. LynkedUp Pro</div>
|
||||
<p style="font-size:16px;color:var(--muted);margin-top:8px">The math is simple.</p>
|
||||
</div>
|
||||
<div class="q-vs-inner">
|
||||
<div class="q-vs-col">
|
||||
<div class="q-vs-label" style="color:var(--red)">❌ What you're paying now</div>
|
||||
<div class="q-vs-items">
|
||||
<div class="q-vs-item"><span class="ico">💸</span><span>EagleView reports - <strong style="color:#fff">$700+/mo</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">💸</span><span>JobNimbus CRM - <strong style="color:#fff">00+/mo</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">💸</span><span>SalesRabbit - <strong style="color:#fff">9+/user/mo</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">💸</span><span>Billing + proposal tools - <strong style="color:#fff">00+/mo</strong></span></div>
|
||||
<div style="border-top:1px solid var(--border);margin-top:12px;padding-top:12px;display:flex;justify-content:space-between">
|
||||
<span style="font-size:14px;color:var(--muted)">Total monthly</span>
|
||||
<span style="font-family:'Barlow Condensed',sans-serif;font-size:22px;font-weight:800;color:var(--red)">,150+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-vs-sep">→</div>
|
||||
<div class="q-vs-col win">
|
||||
<div class="q-vs-label" style="color:var(--green)">✅ LynkedUp Pro</div>
|
||||
<div class="q-vs-items">
|
||||
<div class="q-vs-item"><span class="ico">✅</span><span>AI Measurements - <strong style="color:#fff">$0/report</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">✅</span><span>Roofing CRM - <strong style="color:#fff">included</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">✅</span><span>Hail alerts + canvassing - <strong style="color:#fff">included</strong></span></div>
|
||||
<div class="q-vs-item"><span class="ico">✅</span><span>Billing + proposals - <strong style="color:#fff">included</strong></span></div>
|
||||
<div style="border-top:1px solid rgba(0,229,160,.2);margin-top:12px;padding-top:12px;display:flex;justify-content:space-between">
|
||||
<span style="font-size:14px;color:var(--muted)">Total monthly</span>
|
||||
<span style="font-family:'Barlow Condensed',sans-serif;font-size:22px;font-weight:800;color:var(--green)">49</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="max-width:860px;margin:24px auto 0;background:rgba(0,229,160,.05);border:1px solid rgba(0,229,160,.2);border-radius:12px;padding:18px 28px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px">
|
||||
<span style="font-size:16px;color:var(--muted)">Your monthly savings:</span>
|
||||
<span style="font-family:'Barlow Condensed',sans-serif;font-size:40px;font-weight:900;color:var(--green)">00+ / month</span>
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Claim These Savings →</a>
|
||||
</div>
|
||||
</div>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<section class="q-cta" id="cta">
|
||||
<div style="position:relative;z-index:1">
|
||||
<div style="display:inline-block;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:8px;padding:8px 20px;margin-bottom:20px;font-size:13px;color:var(--orange);font-weight:600">🔥 Only 12 Founding Member Spots Remaining</div>
|
||||
<div class="q-price"><sup style="font-size:32px;color:var(--muted)">$</sup>2000<sub>/mo</sub></div>
|
||||
<div class="q-price-note">Everything included. No per-user fees. No per-report charges. 30-day money-back.</div>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:16px">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary" style="font-size:18px;padding:16px 40px">Book a demo →</a>
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--muted);display:flex;gap:20px;justify-content:center;flex-wrap:wrap">
|
||||
<span>✅ No credit card required</span><span>✅ Free onboarding</span><span>✅ 30-day money-back guarantee</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer style="padding:32px 24px;border-top:1px solid var(--border);text-align:center">
|
||||
<p style="font-size:13px;color:var(--muted)">© 2026 LynkedUp Pro · <a href="lynkeduppro_final.html" style="color:var(--muted)">Full Platform →</a> · <a href="https://book.lynkeduppro.com/" style="color:var(--muted)">Privacy</a></p>
|
||||
</footer>
|
||||
<div class="mob-sticky" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px">
|
||||
<a href="https://book.lynkeduppro.com/" style="flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:15px;text-decoration:none;background:var(--orange);color:#080c14">Book a demo</a>
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add('visible')})},{threshold:.1});
|
||||
document.querySelectorAll('.reveal').forEach(el=>obs.observe(el));
|
||||
if(window.innerWidth<=640)document.querySelector('.mob-sticky').style.display='flex';
|
||||
window.addEventListener('resize',()=>{document.querySelector('.mob-sticky').style.display=window.innerWidth<=640?'flex':'none'});
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,534 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;800;900&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
.shot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border);
|
||||
object-fit: cover;
|
||||
box-shadow: 0 36px 72px rgba(0, 0, 0, .5);
|
||||
}
|
||||
:root{--bg:#05080f;--bg2:#0b1220;--bg3:#101928;--accent:#00c8ff;--orange:#f59e0b;--green:#00e5a0;--red:#ef4444;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#e8edf5;--muted:#8a9bb5;--card:#0d1725;--card2:#111c2e}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Outfit',sans-serif;font-size:16px;line-height:1.7;overflow-x:hidden}
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:200;display:flex;align-items:center;justify-content:space-between;padding:16px 48px;background:rgba(5,8,15,.92);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
|
||||
.logo{display:flex;align-items:center;gap:10px;text-decoration:none}
|
||||
.logo-mark{width:32px;height:32px;background:linear-gradient(135deg,#0ea5e9,#00e5a0);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:900;font-size:16px;color:#fff}
|
||||
.logo-text{font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:18px;color:#fff}
|
||||
.logo-text span{color:var(--orange)}
|
||||
.nav-cta{background:var(--orange);color:#080c14;padding:9px 22px;border-radius:6px;font-weight:600;font-size:14px;text-decoration:none;transition:opacity .2s}
|
||||
.nav-cta:hover{opacity:.85}
|
||||
section{padding:88px 24px}
|
||||
.container{max-width:1080px;margin:0 auto}
|
||||
.s-label{font-size:11px;text-transform:uppercase;letter-spacing:3px;font-weight:600;margin-bottom:12px}
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,4.5vw,56px);font-weight:900;line-height:1;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
.s-sub{font-size:17px;color:var(--muted);margin-top:14px;max-width:500px;line-height:1.65}
|
||||
.btn-primary{background:var(--orange);color:#080c14;padding:14px 32px;border-radius:8px;font-weight:600;text-decoration:none;font-size:16px;display:inline-block;transition:transform .15s,box-shadow .15s;font-family:'Outfit',sans-serif;border:none;cursor:pointer}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 10px 36px rgba(245,158,11,.35)}
|
||||
.btn-ghost{background:transparent;color:#fff;padding:14px 28px;border-radius:8px;font-weight:500;text-decoration:none;font-size:16px;border:1px solid var(--border2);display:inline-block;transition:border-color .2s}
|
||||
.btn-ghost:hover{border-color:rgba(255,255,255,.3)}
|
||||
.reveal{opacity:0;transform:translateY(24px);transition:opacity .6s ease,transform .6s ease}
|
||||
.reveal.visible{opacity:1;transform:none}
|
||||
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
|
||||
.hero-bg{position:absolute;inset:0;pointer-events:none}
|
||||
.hero-grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.022) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px);background-size:64px 64px;mask-image:radial-gradient(ellipse 70% 70% at 50% 40%,black,transparent)}
|
||||
.hero-glow{position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:900px;height:600px}
|
||||
.eyebrow{display:inline-flex;align-items:center;gap:8px;background:rgba(0,200,255,.07);border:1px solid rgba(0,200,255,.18);color:var(--accent);font-size:12px;font-weight:500;padding:5px 16px;border-radius:100px;margin-bottom:24px;letter-spacing:.3px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;animation:blink 2s ease infinite}
|
||||
@keyframes blink{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.7)}}
|
||||
h1{font-family:'Barlow Condensed',sans-serif;font-size:clamp(48px,8vw,92px);font-weight:900;line-height:.93;letter-spacing:-1px;color:#fff;max-width:860px;text-transform:uppercase}
|
||||
.hero-sub{font-size:clamp(16px,2vw,19px);color:var(--muted);max-width:520px;margin:24px auto 0;line-height:1.65}
|
||||
.hero-sub strong{color:var(--text)}
|
||||
.hero-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-top:36px}
|
||||
.micro-row{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px;flex-wrap:wrap}
|
||||
.micro{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
.micro span:first-child{color:var(--green)}
|
||||
.stat-row{display:flex;gap:40px;justify-content:center;margin-top:64px;flex-wrap:wrap}
|
||||
.stat{text-align:center;position:relative}
|
||||
.stat+.stat::before{content:'';position:absolute;left:-20px;top:20%;bottom:20%;width:1px;background:var(--border)}
|
||||
.stat-num{font-family:'Barlow Condensed',sans-serif;font-size:38px;font-weight:900;color:#fff;line-height:1;letter-spacing:-1px}
|
||||
.stat-label{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
.pain-section{background:var(--bg2);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
|
||||
.pain-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--border);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-top:44px}
|
||||
.p-item{background:var(--card);padding:28px 22px}
|
||||
.p-icon{font-size:24px;margin-bottom:12px}
|
||||
.p-item h3{font-family:'Barlow Condensed',sans-serif;font-size:16px;font-weight:700;color:#fff;margin-bottom:7px;text-transform:uppercase}
|
||||
.p-item p{font-size:14px;color:var(--muted);line-height:1.7}
|
||||
.p-cost{display:inline-block;background:rgba(239,68,68,.1);color:var(--red);font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;margin-top:8px;border:1px solid rgba(239,68,68,.2)}
|
||||
.solution-section{background:var(--bg)}
|
||||
.sol-split{display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;margin-top:44px}
|
||||
.sol-points{list-style:none;margin-top:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.sol-points li{display:flex;align-items:flex-start;gap:12px;font-size:15px;color:var(--text)}
|
||||
.sol-points li .ico{font-size:18px;min-width:26px;margin-top:1px}
|
||||
.sol-points li strong{color:#fff}
|
||||
.sol-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:22px}
|
||||
.sol-stat{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px}
|
||||
.sol-stat-num{font-family:'Barlow Condensed',sans-serif;font-size:26px;font-weight:900;line-height:1;margin-bottom:4px}
|
||||
.sol-stat-label{font-size:12px;color:var(--muted)}
|
||||
.mock-card{background:var(--card);border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 36px 72px rgba(0,0,0,.5)}
|
||||
.mock-bar{background:var(--bg2);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.mock-title{font-size:13px;font-weight:600;color:#fff}
|
||||
.mock-badge{font-size:9px;font-weight:700;padding:3px 9px;border-radius:100px}
|
||||
.mock-body{padding:14px}
|
||||
.proof-section{background:var(--bg2)}
|
||||
.proof-card{background:var(--card);border:1px solid rgba(0,200,255,.15);border-radius:18px;padding:36px;max-width:720px;margin:44px auto 0;position:relative}
|
||||
.proof-stars{color:var(--orange);font-size:13px;letter-spacing:2px;margin-bottom:14px}
|
||||
.proof-quote{font-size:17px;color:var(--text);line-height:1.8;font-style:italic;margin-bottom:22px}
|
||||
.proof-author{display:flex;align-items:center;gap:14px;border-top:1px solid var(--border);padding-top:18px}
|
||||
.proof-av{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Barlow Condensed',sans-serif;font-weight:800;font-size:15px;color:#05080f;flex-shrink:0}
|
||||
.proof-name{font-weight:600;font-size:15px;color:#fff}
|
||||
.proof-role{font-size:12px;color:var(--muted)}
|
||||
.proof-result{position:absolute;top:24px;right:24px;background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.18);border-radius:8px;padding:8px 12px;text-align:center}
|
||||
.pr-num{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:800;color:var(--green);line-height:1}
|
||||
.pr-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.3px}
|
||||
.cta-section{background:linear-gradient(135deg,rgba(0,200,255,.05),rgba(0,229,160,.04),rgba(245,158,11,.04));border-top:1px solid var(--border);padding:88px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-section::before{content:'';position:absolute;top:-200px;left:50%;transform:translateX(-50%);width:700px;height:450px;background:radial-gradient(ellipse,rgba(0,200,255,.06),transparent 70%);pointer-events:none}
|
||||
.cta-section h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,60px);font-weight:900;color:#fff;line-height:1;max-width:640px;margin:0 auto 14px;text-transform:uppercase;letter-spacing:-1px}
|
||||
.cta-section h2 em{font-style:normal;color:var(--orange)}
|
||||
.cta-section p{font-size:17px;color:var(--muted);max-width:420px;margin:0 auto 28px}
|
||||
.cta-form{display:flex;gap:10px;max-width:400px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.cta-form input{flex:1;min-width:190px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;padding:12px 16px;color:#fff;font-size:15px;font-family:'Outfit',sans-serif;outline:none;transition:border-color .2s}
|
||||
.cta-form input::placeholder{color:var(--muted)}
|
||||
.cta-form input:focus{border-color:rgba(0,200,255,.4)}
|
||||
.cta-micro{font-size:12px;color:var(--muted);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}
|
||||
footer{padding:40px 24px 28px;border-top:1px solid var(--border);text-align:center}
|
||||
footer p{font-size:13px;color:var(--muted)}
|
||||
footer a{color:var(--muted);text-decoration:none}
|
||||
footer a:hover{color:#fff}
|
||||
.mob-sticky{display:none;position:fixed;bottom:0;left:0;right:0;z-index:150;background:var(--bg2);border-top:1px solid var(--border);padding:12px 16px;gap:10px}
|
||||
.mob-sticky a{flex:1;text-align:center;padding:12px;border-radius:8px;font-weight:600;font-size:14px;text-decoration:none}
|
||||
.mob-sticky .p{background:var(--orange);color:#080c14}
|
||||
.mob-sticky .s{border:1px solid var(--border2);color:#fff}
|
||||
/* ---- Background image helpers ---- */
|
||||
.has-bg{position:relative;overflow:hidden}
|
||||
.has-bg .bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0}
|
||||
.has-bg .bg-overlay{position:absolute;inset:0;z-index:1}
|
||||
.has-bg .container,.has-bg>*:not(.bg-img):not(.bg-overlay):not(.hero-bg){position:relative;z-index:2}
|
||||
/* ---- Enhanced headings ---- */
|
||||
.s-title{font-family:'Barlow Condensed',sans-serif;font-size:clamp(36px,5vw,62px);font-weight:900;line-height:.97;letter-spacing:-.5px;color:#fff;text-transform:uppercase}
|
||||
/* ---- Mid-page CTA strips ---- */
|
||||
.cta-strip{padding:80px 24px;text-align:center;position:relative;overflow:hidden}
|
||||
.cta-strip .bg-img{position:absolute;inset:0;background-size:cover;background-position:center;z-index:0}
|
||||
.cta-strip .bg-overlay{position:absolute;inset:0;z-index:1}
|
||||
.cta-strip .inner{position:relative;z-index:2;max-width:660px;margin:0 auto}
|
||||
.cta-strip-eyebrow{display:inline-flex;align-items:center;gap:7px;padding:5px 14px;border-radius:100px;margin-bottom:20px;font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase}
|
||||
.cta-strip h2{font-family:'Barlow Condensed',sans-serif;font-size:clamp(34px,5vw,58px);font-weight:900;line-height:.97;letter-spacing:-.5px;text-transform:uppercase;color:#fff;margin-bottom:16px}
|
||||
.cta-strip p{font-size:16px;color:var(--muted);line-height:1.7;max-width:480px;margin:0 auto 28px}
|
||||
.cta-strip-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.cta-strip-trust{display:flex;gap:20px;justify-content:center;margin-top:16px;flex-wrap:wrap}
|
||||
.cta-strip-trust span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px}
|
||||
/* ---- Proof section bg ---- */
|
||||
.proof-section{position:relative;overflow:hidden}
|
||||
/* ---- Pain section heading accent line ---- */
|
||||
.accent-line{display:inline-block;width:40px;height:3px;border-radius:2px;margin-bottom:16px;vertical-align:middle}
|
||||
@media(max-width:1280px){section{padding:80px 24px}nav{padding:16px 32px}.cta-strip{padding:72px 24px}}
|
||||
@media(max-width:1024px){section{padding:72px 24px}.pain-grid{grid-template-columns:repeat(2,1fr)}.sol-split{grid-template-columns:1fr;gap:40px}h1{font-size:clamp(42px,7vw,72px)}.s-title{font-size:clamp(30px,4.5vw,48px)}.cta-section h2{font-size:clamp(32px,5vw,52px)}.cta-strip h2{font-size:clamp(30px,5vw,48px)}}
|
||||
@media(max-width:860px){nav{padding:14px 20px}.sol-split{grid-template-columns:1fr!important;gap:36px}.pain-grid{grid-template-columns:1fr!important}.sol-stats{grid-template-columns:1fr 1fr}.mob-sticky{display:flex}body{padding-bottom:72px}section{padding:64px 20px}.hero{padding:100px 16px 60px;min-height:auto}.hero-actions{flex-direction:column;width:100%;max-width:320px;margin-left:auto;margin-right:auto}.hero-actions .btn-primary,.hero-actions .btn-ghost{text-align:center;width:100%}.stat-row{gap:28px}.stat-num{font-size:32px}.cta-section{padding:64px 20px}.cta-strip{padding:64px 20px}.cta-strip-btns{flex-direction:column;width:100%;max-width:320px;margin-left:auto;margin-right:auto}.cta-strip-btns .btn-primary,.cta-strip-btns .btn-ghost{text-align:center;width:100%}.proof-card{padding:28px 22px}.proof-result{position:static;display:inline-block;margin:0 0 14px}}
|
||||
@media(max-width:560px){.stat-row{gap:20px;flex-direction:column}.stat+.stat::before{display:none}h1{font-size:clamp(36px,11vw,64px)}.s-title{font-size:clamp(26px,7.5vw,40px)}.cta-section h2{font-size:clamp(28px,8vw,44px)}.cta-strip h2{font-size:clamp(26px,7.5vw,40px)}.cta-form{flex-direction:column}.cta-form input{min-width:100%;width:100%}.cta-form .btn-primary{width:100%;text-align:center}.sol-stats{grid-template-columns:1fr}.hero-sub{font-size:15px}.micro-row{gap:10px}section{padding:56px 16px}.cta-strip{padding:56px 16px}.hero{padding:96px 16px 48px}.eyebrow{font-size:11px;padding:5px 12px}.proof-quote{font-size:15px}.cta-section p,.cta-strip p{font-size:15px}.cta-strip-trust{gap:10px}}
|
||||
@media(max-width:380px){nav{padding:12px 14px}.nav-cta{padding:8px 14px;font-size:13px}.logo-text{font-size:16px}h1{font-size:clamp(32px,11vw,48px)}.s-title{font-size:24px}.btn-primary,.btn-ghost{padding:12px 22px;font-size:14px}.cta-section h2,.cta-strip h2{font-size:26px}.proof-card{padding:22px 18px}}</style>
|
||||
<title>LynkedUp Pro - Stop Getting Your Insurance Claims Underpaid</title>
|
||||
<meta name="description" content="AI-annotated hail impact documentation and LiDAR digital twins get your insurance claims approved on the first submission. Stop leaving money on the table."/>
|
||||
</head><body>
|
||||
<nav>
|
||||
<a href="https://book.lynkeduppro.com/" class="logo"><img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro" class="brand-logo-img" style="height:34px;width:auto;display:block;object-fit:contain"></a>
|
||||
<a href="https://launch.lynkeduppro.com/" class="nav-cta">Watch a demo</a> <a href="https://book.lynkeduppro.com/" class="nav-cta">Book a demo</a>
|
||||
</nav>
|
||||
<section class="hero has-bg" id="home">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1800&q=80');filter:brightness(.13) saturate(.5)"></div>
|
||||
<div class="bg-overlay" style="background:linear-gradient(180deg,rgba(5,8,15,.6) 0%,rgba(5,8,15,.85) 60%,rgba(5,8,15,1) 100%)"></div>
|
||||
<div class="hero-bg"><div class="hero-grid"></div><div class="hero-glow" style="background:radial-gradient(ellipse,rgba(245,158,11,.12),transparent 65%)"></div></div>
|
||||
<div class="eyebrow"><span class="dot" style="background:var(--orange)"></span> Adjuster-Ready Reports · Insurance Claims</div>
|
||||
<h1>The Insurance Company<br>Isn't Cheating You -<br><span style="color:var(--orange)">Your Docs Are.</span></h1>
|
||||
<p class="hero-sub">Adjusters need <strong>precise, annotated evidence</strong> to pay full value. Without AI-documented hail impacts and LiDAR data, you hand them every reason to underpay, and they take it. It's your assistant that never sleeps or takes a day off.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Fix My Claims Process →</a>
|
||||
</div>
|
||||
<div class="micro-row" style="margin-top:14px">
|
||||
<span class="micro" style="color:var(--muted)">4.9★ · 248 reviews</span>
|
||||
</div>
|
||||
<div class="micro-row">
|
||||
<span class="micro"><span>✅</span> First-submission approval</span>
|
||||
<span class="micro"><span>✅</span> AI-annotated hail impacts</span>
|
||||
<span class="micro"><span>✅</span> Xactimate-style output</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat"><div class="stat-num" style="color:var(--green)">1st</div><div class="stat-label">Submission approval rate</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--orange)">$0</div><div class="stat-label">Manual documentation time</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:var(--accent)">1-click</div><div class="stat-label">Generate full adjuster report</div></div>
|
||||
<div class="stat"><div class="stat-num" style="color:#fff">100%</div><div class="stat-label">Insurance-accepted format</div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pain-section has-bg reveal" id="problem">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504309092620-4d0ec726efa4?w=1800&q=80');filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-overlay" style="background:linear-gradient(135deg,rgba(11,18,32,.97) 0%,rgba(11,18,32,.88) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--red)">The Problem</div>
|
||||
<h2 class="s-title">Every Rejected Claim Is<br><span style="color:var(--red)">Money You've Already Earned.</span></h2>
|
||||
<p class="s-sub">You did the work. You climbed the roof. You filed the report. And the adjuster still came back with a low number, or nothing at all. It's not the damage that's the problem. It's the paper trail.</p>
|
||||
<div class="pain-grid">
|
||||
<div class="p-item"><div class="p-icon">📋</div><h3>Vague Reports Get Rejected</h3><p>Adjuster sees "hail damage noted" with a few phone photos. They write back for more evidence, delay the claim, or simply underpay. You have no leverage.</p><span class="p-cost">Cost: Weeks of delays, underpayment</span></div>
|
||||
<div class="p-item"><div class="p-icon">🔍</div><h3>You Miss Impact Zones</h3><p>Manual roof inspections miss impacts on complex roof geometries. AI catches what the human eye skips, and insurance adjusters know the difference.</p><span class="p-cost">Cost: Unclaimed damage per job</span></div>
|
||||
<div class="p-item"><div class="p-icon">📂</div><h3>Hours of Manual Documentation</h3><p>You do the roof work in 4 minutes. Then you spend 3 hours compiling photos, measurements, storm data and formatting a report adjusters may still reject.</p><span class="p-cost">Cost: 3+ hours per claim</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===== CTA STRIP 1 ===== -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1527689368864-3a821dbccc34?w=1800&q=80');position:absolute;inset:0;background-size:cover;background-position:center;filter:brightness(.08) saturate(.4)"></div>
|
||||
<div class="bg-overlay" style="position:absolute;inset:0;background:linear-gradient(135deg,rgba(5,8,15,.97) 0%,rgba(15,8,5,.95) 100%)"></div>
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:350px;background:radial-gradient(ellipse,rgba(239,68,68,.07),transparent 70%);pointer-events:none;z-index:1"></div>
|
||||
<div class="inner">
|
||||
<div class="cta-strip-eyebrow" style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);color:var(--red)">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:var(--red);animation:blink 2s ease infinite;display:inline-block"></span>
|
||||
Right Now, This Storm Season
|
||||
</div>
|
||||
<h2>You're Handing Adjusters<br>a <span style="color:var(--red)">Reason to Say No.</span><br><span style="color:var(--orange)">Stop It Today.</span></h2>
|
||||
<p>Every claim you file without AI-annotated hail data is a claim the adjuster has permission to shrink. The good news? One 30-minute demo changes the entire outcome of your next job.</p>
|
||||
<div class="cta-strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Show Me How to Fix It →</a>
|
||||
|
||||
</div>
|
||||
<div class="cta-strip-trust">
|
||||
<span><span style="color:var(--green)">✅</span> No credit card</span>
|
||||
<span><span style="color:var(--green)">✅</span> 30-min live demo</span>
|
||||
<span><span style="color:var(--green)">✅</span> Real claims results</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="solution-section has-bg reveal" id="solution">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504307651254-35680f356dfd?w=1800&q=80');filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-overlay" style="background:linear-gradient(90deg,rgba(5,8,15,1) 45%,rgba(5,8,15,.75) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--orange)">The Fix</div>
|
||||
<h2 class="s-title">One Scan.<br><span style="color:var(--orange)">Full Payout.</span><br>Zero Resubmissions.</h2>
|
||||
<div class="sol-split">
|
||||
<div>
|
||||
<p class="s-sub">LynkedUp Pro combines LiDAR drone scanning, AI hail impact annotation, and historical storm data into a single adjuster-ready PDF. Automatically. Every time.</p>
|
||||
<ul class="sol-points">
|
||||
<li><span class="ico">🤖</span><span><strong>AI auto-annotates every impact.</strong> The digital twin flags hail strikes, granule loss, and ridge damage with exact GPS coordinates. Nothing gets missed.</span></li>
|
||||
<li><span class="ico">🌩️</span><span><strong>Storm data auto-attached.</strong> HailTrace and NOAA historical records pull automatically into every report. Dated, sourced, irrefutable.</span></li>
|
||||
<li><span class="ico">📄</span><span><strong>Xactimate-style output.</strong> One click generates a formatted adjuster report with the exact structure insurance companies require. No resubmissions.</span></li>
|
||||
<li><span class="ico">🔗</span><span><strong>Wired into your CRM.</strong> Report links to the job file, the homeowner, the estimate, and the signed contract. Everything auditors want, one click away.</span></li>
|
||||
</ul>
|
||||
<div class="sol-stats">
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--green)">1-Click</div><div class="sol-stat-label">Report generation</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--orange)">0 hrs</div><div class="sol-stat-label">Manual documentation time</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:var(--accent)">100%</div><div class="sol-stat-label">Insurance-accepted format</div></div>
|
||||
<div class="sol-stat"><div class="sol-stat-num" style="color:#fff">Auto</div><div class="sol-stat-label">Storm data attached</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<img src="/page/assets/esitimate.png" alt="LynkedUp Pro AI-annotated adjuster report" class="shot" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- ===== CTA STRIP 2 ===== -->
|
||||
<section class="cta-strip">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1800&q=80');position:absolute;inset:0;background-size:cover;background-position:center;filter:brightness(.1) saturate(.4)"></div>
|
||||
<div class="bg-overlay" style="position:absolute;inset:0;background:linear-gradient(135deg,rgba(5,8,15,.96) 0%,rgba(5,12,8,.95) 100%)"></div>
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:700px;height:350px;background:radial-gradient(ellipse,rgba(0,229,160,.06),transparent 70%);pointer-events:none;z-index:1"></div>
|
||||
<div class="inner">
|
||||
<div class="cta-strip-eyebrow" style="background:rgba(0,229,160,.08);border:1px solid rgba(0,229,160,.2);color:var(--green)">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:var(--green);animation:blink 2s ease infinite;display:inline-block"></span>
|
||||
Your Adjuster Wants a Reason to Say Yes
|
||||
</div>
|
||||
<h2>Give Them <span style="color:var(--green)">Irrefutable</span><br>Evidence - or Keep<br><span style="color:var(--orange)">Fighting for Every Dollar.</span></h2>
|
||||
<p>LynkedUp Pro gives adjusters exactly what they need to approve on the first pass: GPS-pinned impacts, NOAA storm data, and Xactimate-ready formatting. Generated automatically. Every job.</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;margin-bottom:28px">
|
||||
<div style="background:var(--card);border:1px solid rgba(0,229,160,.18);border-radius:10px;padding:14px 22px;text-align:center;min-width:120px">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--green);line-height:1">100%</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">First-Pass Rate</div>
|
||||
</div>
|
||||
<div style="background:var(--card);border:1px solid rgba(245,158,11,.18);border-radius:10px;padding:14px 22px;text-align:center;min-width:120px">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--orange);line-height:1">$2.1K</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">Saved / Month</div>
|
||||
</div>
|
||||
<div style="background:var(--card);border:1px solid rgba(0,200,255,.18);border-radius:10px;padding:14px 22px;text-align:center;min-width:120px">
|
||||
<div style="font-family:'Barlow Condensed',sans-serif;font-size:28px;font-weight:900;color:var(--accent);line-height:1">0 hrs</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px;text-transform:uppercase;letter-spacing:.5px">Manual Doc Time</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cta-strip-btns">
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary">Get the Dashboard Free →</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="proof-section has-bg reveal" id="proof">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=1800&q=80');filter:brightness(.09) saturate(.4)"></div>
|
||||
<div class="bg-overlay" style="background:linear-gradient(180deg,rgba(11,18,32,1) 0%,rgba(11,18,32,.92) 60%,rgba(11,18,32,1) 100%)"></div>
|
||||
<div class="container">
|
||||
<div class="s-label" style="color:var(--orange);text-align:center">Real Result</div>
|
||||
<h2 class="s-title" style="text-align:center">Our Clients Don't<br>Resubmit. <span style="color:var(--orange)">They Get Paid.</span></h2>
|
||||
<div class="proof-card" style="border-color:rgba(245,158,11,.2)">
|
||||
<div class="proof-result" style="border-color:rgba(245,158,11,.18);background:rgba(245,158,11,.06)"><div class="pr-num" style="color:var(--orange)">$2.1K</div><div class="pr-label">Saved/Month</div></div>
|
||||
<div class="proof-stars">★★★★★</div>
|
||||
<p class="proof-quote">"We used to resubmit claims 2-3 times per job. Adjusters kept asking for more evidence we didn't have. With LynkedUp Pro, the AI-annotated reports have the exact documentation they need. Every claim gets approved on the first submission now. That alone is worth the entire subscription."</p>
|
||||
<div class="proof-author">
|
||||
<div class="proof-av" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MT</div>
|
||||
<div><div class="proof-name">Marcus Thompson</div><div class="proof-role">Operations Manager · Fort Worth, TX</div></div>
|
||||
</div>
|
||||
<span style="position:absolute;right:10px;bottom:8px;font-size:11px;opacity:.5">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="roofing-offer-section" id="pricing">
|
||||
<div class="container">
|
||||
|
||||
<!-- TOP HEADING -->
|
||||
<div class="text-center roofing-heading mb-5">
|
||||
<span class="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<!-- MAIN GRID -->
|
||||
<div class="row g-4 align-items-stretch">
|
||||
|
||||
<!-- LEFT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="lifetime-card h-100">
|
||||
<div class="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p class="small-text">One payment. Unlimited operational control.</p>
|
||||
<div class="price-box">
|
||||
<h2>$2000</h2>
|
||||
<span>PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" class="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT CARD -->
|
||||
<div class="col-lg-6">
|
||||
<div class="roofing-info-card h-100">
|
||||
<span class="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p class="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div class="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TERMS (native <details> accordion - no JS needed) -->
|
||||
<div class="terms-wrapper mt-4">
|
||||
<details class="custom-accordion-item">
|
||||
<summary class="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div class="accordion-body">
|
||||
<div class="deal-terms-legal" style="font-size:12px;line-height:1.65;color:#444">
|
||||
<p style="margin:0 0 10px">All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p style="margin:0 0 10px">Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p style="margin:0 0 10px">The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p style="margin:0 0 10px">SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p style="margin:0 0 10px">Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style="margin:0">Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div class="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" class="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://www.lynkeduppro.com/contact" class="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
/* ===== Roofing Offer / Pricing - compact, self-contained ===== */
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<section class="cta-section has-bg" id="cta">
|
||||
<div class="bg-img" style="background-image:url('https://images.unsplash.com/photo-1504309092620-4d0ec726efa4?w=1800&q=80');filter:brightness(.08) saturate(.3)"></div>
|
||||
<div class="bg-overlay" style="background:linear-gradient(135deg,rgba(0,200,255,.04),rgba(0,229,160,.03),rgba(245,158,11,.04))"></div>
|
||||
<div class="container" style="position:relative;z-index:2">
|
||||
<div style="display:inline-block;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:8px;padding:8px 18px;margin-bottom:24px;font-size:13px;color:var(--orange);font-weight:600">📋 Stop resubmitting claims that should've been approved the first time</div>
|
||||
<h2>Every Dollar Is Yours.<br><em>Start Claiming It.</em></h2>
|
||||
<p>See how LynkedUp Pro generates bulletproof adjuster reports automatically, in a 30-minute demo.</p>
|
||||
<div class="cta-form">
|
||||
|
||||
<a href="https://book.lynkeduppro.com/" class="btn-primary" onclick="event.preventDefault()">Fix My Claims Process →</a>
|
||||
</div>
|
||||
<div class="cta-micro"><span>✅ No credit card</span><span>✅ 30-day money-back guarantee</span><span>✅ Free onboarding</span></div>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<p>© 2026 LynkedUp Pro · See Full Platform →</p>
|
||||
<p style="margin-top:8px;font-size:12px;opacity:.5">$349/mo flat rate · No credit card required · 30-day money-back guarantee · Built for roofing</p>
|
||||
<p style="margin-top:8px;font-size:11px;opacity:.5">* Testimonials and figures shown are illustrative and reflect beta-period results; individual results vary.</p>
|
||||
</footer>
|
||||
<div class="mob-sticky">
|
||||
<a href="https://book.lynkeduppro.com/" class="p">Book a demo</a>
|
||||
<a href="lynkeduppro_final.html" class="s">See Full Platform</a>
|
||||
</div>
|
||||
<script>
|
||||
const obs=new IntersectionObserver(e=>{e.forEach(el=>{if(el.isIntersecting)el.target.classList.add("visible")})},{threshold:.1});
|
||||
document.querySelectorAll(".reveal").forEach(el=>obs.observe(el));
|
||||
document.querySelectorAll("a[href^=\"#\"]").forEach(a=>{a.addEventListener("click",function(e){const t=document.querySelector(this.getAttribute("href"));if(t){e.preventDefault();t.scrollIntoView({behavior:"smooth",block:"start"});}}); });
|
||||
</script>
|
||||
<!-- FLOATING LIFETIME DEAL BUTTON -->
|
||||
<a href="#pricing" id="floatingDealBtn">
|
||||
🔥 Founders Lifetime Deal - $2000
|
||||
</a>
|
||||
|
||||
<style>
|
||||
#floatingDealBtn {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
background: linear-gradient(135deg,#0ea5e9,#2563eb);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 10px 30px rgba(14,165,233,0.35);
|
||||
z-index: 9999;
|
||||
transition: all 0.3s ease;
|
||||
animation: floatPulse 2s infinite;
|
||||
}
|
||||
|
||||
#floatingDealBtn:hover {
|
||||
transform: translateY(-4px) scale(1.03);
|
||||
box-shadow: 0 16px 40px rgba(14,165,233,0.45);
|
||||
}
|
||||
|
||||
@keyframes floatPulse {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width:768px) {
|
||||
#floatingDealBtn {
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Hosted checkout (Stripe) for the "Claim Founders Lifetime Deal" button -->
|
||||
<script src="/square-checkout.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 1009 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 7.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,641 @@
|
||||
/**
|
||||
* <image-slot> — user-fillable image placeholder.
|
||||
*
|
||||
* Drop this into a deck, mockup, or page wherever you want the user to
|
||||
* supply an image. You control the slot's shape and size; the user fills it
|
||||
* by dragging an image file onto it (or clicking to browse). The dropped
|
||||
* image persists across reloads via a .image-slots.state.json sidecar —
|
||||
* same read-via-fetch / write-via-window.omelette pattern as
|
||||
* design_canvas.jsx, so the filled slot shows on share links, downloaded
|
||||
* zips, and PPTX export. Outside the omelette runtime the slot is read-only.
|
||||
*
|
||||
* The host bridge only allows sidecar writes at the project root, so the
|
||||
* HTML that uses this component is assumed to live at the project root too
|
||||
* (same constraint as design_canvas.jsx).
|
||||
*
|
||||
* Attributes:
|
||||
* id Persistence key. REQUIRED for the drop to survive reload —
|
||||
* every slot on the page needs a distinct id.
|
||||
* shape 'rect' | 'rounded' | 'circle' | 'pill' (default 'rounded')
|
||||
* 'circle' applies 50% border-radius; on a non-square slot
|
||||
* that's an ellipse — set equal width and height for a true
|
||||
* circle.
|
||||
* radius Corner radius in px for 'rounded'. (default 12)
|
||||
* mask Any CSS clip-path value. Overrides `shape` — use this for
|
||||
* hexagons, blobs, arbitrary polygons.
|
||||
* fit object-fit: cover | contain | fill. (default 'cover')
|
||||
* With cover (the default) double-clicking the filled slot
|
||||
* enters a reframe mode: the whole image spills past the mask
|
||||
* (translucent outside, opaque inside), drag to reposition,
|
||||
* corner-drag to scale. The crop persists alongside the image
|
||||
* in the sidecar. contain/fill stay static.
|
||||
* position object-position for fit=contain|fill. (default '50% 50%')
|
||||
* placeholder Empty-state caption. (default 'Drop an image')
|
||||
* src Optional initial/fallback image URL. A user drop overrides
|
||||
* it; clearing the drop reveals src again.
|
||||
*
|
||||
* Size and layout come from ordinary CSS on the element — width/height
|
||||
* inline or from a parent grid — so it composes with any layout.
|
||||
*
|
||||
* Usage:
|
||||
* <script src="image-slot.js"></script>
|
||||
* <image-slot id="hero" style="width:800px;height:450px" shape="rounded" radius="20"
|
||||
* placeholder="Drop a hero image"></image-slot>
|
||||
* <image-slot id="avatar" style="width:120px;height:120px" shape="circle"></image-slot>
|
||||
* <image-slot id="kite" style="width:300px;height:300px"
|
||||
* mask="polygon(50% 0, 100% 50%, 50% 100%, 0 50%)"></image-slot>
|
||||
*/
|
||||
|
||||
(() => {
|
||||
const STATE_FILE = '.image-slots.state.json';
|
||||
// 2× a ~600px slot in a 1920-wide deck — retina-sharp without making the
|
||||
// sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB.
|
||||
const MAX_DIM = 1200;
|
||||
// Raster formats only. SVG is excluded (can carry script; createImageBitmap
|
||||
// on SVG blobs is inconsistent). GIF is excluded because the canvas
|
||||
// re-encode keeps only the first frame, so an animated GIF would silently
|
||||
// go still — better to reject than surprise.
|
||||
const ACCEPT = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
|
||||
|
||||
// ── Shared sidecar store ────────────────────────────────────────────────
|
||||
// One fetch + immediate write-on-change for every <image-slot> on the
|
||||
// page. Reads via fetch() so viewing works anywhere the HTML and sidecar
|
||||
// are served together; writes go through window.omelette.writeFile, which
|
||||
// the host allowlists to *.state.json basenames only.
|
||||
const subs = new Set();
|
||||
let slots = {};
|
||||
// ids explicitly cleared before the sidecar fetch resolved — otherwise
|
||||
// the merge below can't tell "never set" from "just deleted" and would
|
||||
// resurrect the sidecar's stale value.
|
||||
const tombstones = new Set();
|
||||
let loaded = false;
|
||||
let loadP = null;
|
||||
|
||||
function load() {
|
||||
if (loadP) return loadP;
|
||||
loadP = fetch(STATE_FILE)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((j) => {
|
||||
// Merge: sidecar loses to any in-memory change that raced ahead of
|
||||
// the fetch (drop or clear) so neither is clobbered by hydration.
|
||||
if (j && typeof j === 'object') {
|
||||
const merged = Object.assign({}, j, slots);
|
||||
// A framing-only write that raced ahead of hydration must not
|
||||
// drop a user image that's only on disk — inherit u from the
|
||||
// sidecar for any in-memory entry that lacks one.
|
||||
for (const k in slots) {
|
||||
if (merged[k] && !merged[k].u && j[k]) {
|
||||
merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u;
|
||||
}
|
||||
}
|
||||
for (const id of tombstones) delete merged[id];
|
||||
slots = merged;
|
||||
}
|
||||
tombstones.clear();
|
||||
})
|
||||
.catch(() => {})
|
||||
.then(() => { loaded = true; subs.forEach((fn) => fn()); });
|
||||
return loadP;
|
||||
}
|
||||
|
||||
// Serialize writes so two near-simultaneous drops on different slots
|
||||
// can't reorder at the backend and leave the sidecar with only the
|
||||
// first. A save requested mid-flight just marks dirty and re-fires on
|
||||
// completion with the then-current slots.
|
||||
let saving = false;
|
||||
let saveDirty = false;
|
||||
function save() {
|
||||
if (saving) { saveDirty = true; return; }
|
||||
const w = window.omelette && window.omelette.writeFile;
|
||||
if (!w) return;
|
||||
saving = true;
|
||||
Promise.resolve(w(STATE_FILE, JSON.stringify(slots)))
|
||||
.catch(() => {})
|
||||
.then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } });
|
||||
}
|
||||
|
||||
const S_MAX = 5;
|
||||
const clampS = (s) => Math.max(1, Math.min(S_MAX, s));
|
||||
|
||||
// Normalize a stored slot value. Pre-reframe sidecars stored a bare
|
||||
// data-URL string; newer ones store {u, s, x, y}. Either shape is valid.
|
||||
function getSlot(id) {
|
||||
const v = slots[id];
|
||||
if (!v) return null;
|
||||
return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v;
|
||||
}
|
||||
|
||||
function setSlot(id, val) {
|
||||
if (!id) return;
|
||||
if (val) { slots[id] = val; tombstones.delete(id); }
|
||||
else { delete slots[id]; if (!loaded) tombstones.add(id); }
|
||||
subs.forEach((fn) => fn());
|
||||
// A drop is rare + high-value — write immediately so nav-away can't lose
|
||||
// it. Gate on the initial read so we don't overwrite a sidecar we haven't
|
||||
// merged yet; the merge in load() keeps this change once the read lands.
|
||||
if (loaded) save(); else load().then(save);
|
||||
}
|
||||
|
||||
// ── Image downscale ─────────────────────────────────────────────────────
|
||||
// Encode through a canvas so the sidecar carries resized bytes, not the
|
||||
// raw upload. Longest side is capped at 2× the slot's rendered width
|
||||
// (retina) and at MAX_DIM. WebP keeps alpha and is ~10× smaller than PNG
|
||||
// for photos, so there's no need for per-image format picking.
|
||||
async function toDataUrl(file, targetW) {
|
||||
const bitmap = await createImageBitmap(file);
|
||||
try {
|
||||
const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM);
|
||||
const scale = Math.min(1, cap / Math.max(bitmap.width, bitmap.height));
|
||||
const w = Math.max(1, Math.round(bitmap.width * scale));
|
||||
const h = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
|
||||
return canvas.toDataURL('image/webp', 0.85);
|
||||
} finally {
|
||||
bitmap.close && bitmap.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Custom element ──────────────────────────────────────────────────────
|
||||
const stylesheet =
|
||||
':host{display:inline-block;position:relative;vertical-align:top;' +
|
||||
' font:13px/1.3 system-ui,-apple-system,sans-serif;color:rgba(0,0,0,.55);width:240px;height:160px}' +
|
||||
'.frame{position:absolute;inset:0;overflow:hidden;background:rgba(0,0,0,.04)}' +
|
||||
// .frame img (clipped) and .spill (unclipped ghost + handles) share the
|
||||
// same left/top/width/height in frame-%, computed by _applyView(), so the
|
||||
// inside-mask crop and the outside-mask spill stay pixel-aligned.
|
||||
'.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' +
|
||||
' -webkit-user-drag:none;user-select:none;touch-action:none}' +
|
||||
// Reframe mode (double-click): the full image spills past the mask. The
|
||||
// spill layer is sized to the IMAGE bounds so its corners are where the
|
||||
// resize handles belong. The ghost <img> inside is translucent; the real
|
||||
// clipped <img> underneath shows the opaque in-mask crop.
|
||||
'.spill{position:absolute;transform:translate(-50%,-50%);display:none;z-index:1;' +
|
||||
' cursor:grab;touch-action:none}' +
|
||||
':host([data-panning]) .spill{cursor:grabbing}' +
|
||||
'.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' +
|
||||
' pointer-events:none;-webkit-user-drag:none;user-select:none;' +
|
||||
' box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' +
|
||||
'.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' +
|
||||
' background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' +
|
||||
' transform:translate(-50%,-50%)}' +
|
||||
'.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' +
|
||||
'.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' +
|
||||
'.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' +
|
||||
'.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' +
|
||||
':host([data-reframe]){z-index:10}' +
|
||||
':host([data-reframe]) .spill{display:block}' +
|
||||
':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' +
|
||||
'.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +
|
||||
' justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +
|
||||
' cursor:pointer;user-select:none}' +
|
||||
'.empty svg{opacity:.45}' +
|
||||
'.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' +
|
||||
'.empty .sub{font-size:11px}' +
|
||||
'.empty .sub u{text-underline-offset:2px;text-decoration-color:rgba(0,0,0,.25)}' +
|
||||
'.empty:hover .sub u{color:rgba(0,0,0,.75);text-decoration-color:currentColor}' +
|
||||
':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' +
|
||||
' background:rgba(201,100,66,.10)}' +
|
||||
'.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed rgba(0,0,0,.25);' +
|
||||
' transition:border-color .12s}' +
|
||||
':host([data-over]) .ring{border-color:#c96442}' +
|
||||
':host([data-filled]) .ring{display:none}' +
|
||||
// Controls sit BELOW the mask (top:100%), absolutely positioned so the
|
||||
// author-declared slot height is unaffected. The gap is padding, not a
|
||||
// top offset, so the hover target stays contiguous with the frame.
|
||||
'.ctl{position:absolute;top:100%;left:50%;transform:translateX(-50%);padding-top:8px;' +
|
||||
' display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' +
|
||||
' white-space:nowrap}' +
|
||||
':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' +
|
||||
' {opacity:1;pointer-events:auto}' +
|
||||
'.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' +
|
||||
' background:rgba(0,0,0,.65);color:#fff;font:11px/1 system-ui,-apple-system,sans-serif;' +
|
||||
' backdrop-filter:blur(6px)}' +
|
||||
'.ctl button:hover{background:rgba(0,0,0,.8)}' +
|
||||
'.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' +
|
||||
' background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}';
|
||||
|
||||
const icon =
|
||||
'<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
|
||||
'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' +
|
||||
'<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/>' +
|
||||
'<path d="m21 15-5-5L5 21"/></svg>';
|
||||
|
||||
class ImageSlot extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['shape', 'radius', 'mask', 'fit', 'position', 'placeholder', 'src', 'id'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const root = this.attachShadow({ mode: 'open' });
|
||||
// .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius
|
||||
// on the frame (circle, pill, rounded) can't clip them.
|
||||
root.innerHTML =
|
||||
'<style>' + stylesheet + '</style>' +
|
||||
'<div class="frame" part="frame">' +
|
||||
' <img part="image" alt="" draggable="false" style="display:none">' +
|
||||
' <div class="empty" part="empty">' + icon +
|
||||
' <div class="cap"></div>' +
|
||||
' <div class="sub">or <u>browse files</u></div></div>' +
|
||||
' <div class="ring" part="ring"></div>' +
|
||||
'</div>' +
|
||||
'<div class="spill">' +
|
||||
' <img class="ghost" alt="" draggable="false">' +
|
||||
' <div class="handle" data-c="nw"></div><div class="handle" data-c="ne"></div>' +
|
||||
' <div class="handle" data-c="sw"></div><div class="handle" data-c="se"></div>' +
|
||||
'</div>' +
|
||||
'<div class="ctl"><button data-act="replace" title="Replace image">Replace</button>' +
|
||||
' <button data-act="clear" title="Remove image">Remove</button></div>' +
|
||||
'<input type="file" accept="' + ACCEPT.join(',') + '" hidden>';
|
||||
this._frame = root.querySelector('.frame');
|
||||
this._ring = root.querySelector('.ring');
|
||||
this._img = root.querySelector('.frame img');
|
||||
this._empty = root.querySelector('.empty');
|
||||
this._cap = root.querySelector('.cap');
|
||||
this._sub = root.querySelector('.sub');
|
||||
this._spill = root.querySelector('.spill');
|
||||
this._ghost = root.querySelector('.ghost');
|
||||
this._err = null;
|
||||
this._input = root.querySelector('input');
|
||||
this._depth = 0;
|
||||
this._gen = 0;
|
||||
this._view = { s: 1, x: 0, y: 0 };
|
||||
this._subFn = () => this._render();
|
||||
// Shadow-DOM listeners live with the shadow DOM — bound once here so
|
||||
// disconnect/reconnect (e.g. React remount) doesn't stack handlers.
|
||||
this._empty.addEventListener('click', () => this._input.click());
|
||||
root.addEventListener('click', (e) => {
|
||||
const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
|
||||
if (act === 'replace') { this._exitReframe(true); this._input.click(); }
|
||||
if (act === 'clear') {
|
||||
this._exitReframe(false);
|
||||
this._gen++;
|
||||
this._local = null;
|
||||
if (this.id) setSlot(this.id, null); else this._render();
|
||||
}
|
||||
});
|
||||
this._input.addEventListener('change', () => {
|
||||
const f = this._input.files && this._input.files[0];
|
||||
if (f) this._ingest(f);
|
||||
this._input.value = '';
|
||||
});
|
||||
// naturalWidth/Height aren't known until load — re-apply so the cover
|
||||
// baseline is computed from real dimensions, not the 100%×100% fallback.
|
||||
this._img.addEventListener('load', () => this._applyView());
|
||||
// Gated on editable + fit=cover so share links and contain/fill slots
|
||||
// stay static.
|
||||
this.addEventListener('dblclick', (e) => {
|
||||
if (!this.hasAttribute('data-editable') || !this._reframes()) return;
|
||||
e.preventDefault();
|
||||
if (this.hasAttribute('data-reframe')) this._exitReframe(true);
|
||||
else this._enterReframe();
|
||||
});
|
||||
// Pan + resize both originate on the spill layer. A handle pointerdown
|
||||
// drives an aspect-locked resize anchored at the opposite corner; any
|
||||
// other pointerdown on the spill pans. Offsets are frame-% so a
|
||||
// reframed slot survives responsive resize / PPTX export.
|
||||
this._spill.addEventListener('pointerdown', (e) => {
|
||||
if (e.button !== 0 || !this.hasAttribute('data-reframe')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._spill.setPointerCapture(e.pointerId);
|
||||
const rect = this.getBoundingClientRect();
|
||||
const fw = rect.width || 1, fh = rect.height || 1;
|
||||
const corner = e.target.getAttribute && e.target.getAttribute('data-c');
|
||||
let move;
|
||||
if (corner) {
|
||||
// Resize about the OPPOSITE corner. Viewport-px throughout (rect
|
||||
// fw/fh, not clientWidth) so the math survives a transform:scale()
|
||||
// ancestor — deck_stage renders slides scaled-to-fit.
|
||||
const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1;
|
||||
const base = Math.max(fw / iw, fh / ih);
|
||||
const sx = corner.includes('e') ? 1 : -1;
|
||||
const sy = corner.includes('s') ? 1 : -1;
|
||||
const s0 = this._view.s;
|
||||
const w0 = iw * base * s0, h0 = ih * base * s0;
|
||||
const cx0 = (50 + this._view.x) / 100 * fw;
|
||||
const cy0 = (50 + this._view.y) / 100 * fh;
|
||||
const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2;
|
||||
const diag0 = Math.hypot(w0, h0);
|
||||
const ux = sx * w0 / diag0, uy = sy * h0 / diag0;
|
||||
move = (ev) => {
|
||||
const proj = (ev.clientX - rect.left - ox) * ux +
|
||||
(ev.clientY - rect.top - oy) * uy;
|
||||
const s = clampS(s0 * proj / diag0);
|
||||
const d = diag0 * s / s0;
|
||||
this._view.s = s;
|
||||
this._view.x = (ox + ux * d / 2) / fw * 100 - 50;
|
||||
this._view.y = (oy + uy * d / 2) / fh * 100 - 50;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
};
|
||||
} else {
|
||||
this.setAttribute('data-panning', '');
|
||||
const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y };
|
||||
move = (ev) => {
|
||||
this._view.x = start.x + (ev.clientX - start.px) / fw * 100;
|
||||
this._view.y = start.y + (ev.clientY - start.py) / fh * 100;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
};
|
||||
}
|
||||
const up = () => {
|
||||
try { this._spill.releasePointerCapture(e.pointerId); } catch {}
|
||||
this._spill.removeEventListener('pointermove', move);
|
||||
this._spill.removeEventListener('pointerup', up);
|
||||
this._spill.removeEventListener('pointercancel', up);
|
||||
this.removeAttribute('data-panning');
|
||||
this._dragUp = null;
|
||||
};
|
||||
// Stashed so _exitReframe (Escape / outside-click mid-drag) can
|
||||
// tear the capture + listeners down synchronously.
|
||||
this._dragUp = up;
|
||||
this._spill.addEventListener('pointermove', move);
|
||||
this._spill.addEventListener('pointerup', up);
|
||||
this._spill.addEventListener('pointercancel', up);
|
||||
});
|
||||
// Wheel zoom stays available inside reframe mode as a trackpad nicety —
|
||||
// zooms toward the cursor (offset' = cursor·(1-k) + offset·k).
|
||||
this.addEventListener('wheel', (e) => {
|
||||
if (!this.hasAttribute('data-reframe')) return;
|
||||
e.preventDefault();
|
||||
const r = this.getBoundingClientRect();
|
||||
const cx = (e.clientX - r.left) / r.width * 100 - 50;
|
||||
const cy = (e.clientY - r.top) / r.height * 100 - 50;
|
||||
const prev = this._view.s;
|
||||
const next = clampS(prev * Math.pow(1.0015, -e.deltaY));
|
||||
if (next === prev) return;
|
||||
const k = next / prev;
|
||||
this._view.s = next;
|
||||
this._view.x = cx * (1 - k) + this._view.x * k;
|
||||
this._view.y = cy * (1 - k) + this._view.y * k;
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Warn once per page — an id-less slot works for the session but
|
||||
// cannot persist, and two id-less slots would share nothing.
|
||||
if (!this.id && !ImageSlot._warned) {
|
||||
ImageSlot._warned = true;
|
||||
console.warn('<image-slot> without an id will not persist its dropped image.');
|
||||
}
|
||||
this.addEventListener('dragenter', this);
|
||||
this.addEventListener('dragover', this);
|
||||
this.addEventListener('dragleave', this);
|
||||
this.addEventListener('drop', this);
|
||||
subs.add(this._subFn);
|
||||
// width%/height% in _applyView encode the frame aspect at call time —
|
||||
// a host resize (responsive grid, pane divider) would stretch the
|
||||
// image until the next _render. Re-render on size change: _render()
|
||||
// re-seeds _view from stored before clamp/apply, so a shrink→grow
|
||||
// cycle round-trips instead of ratcheting x/y toward the narrower
|
||||
// frame's clamp range.
|
||||
this._ro = new ResizeObserver(() => this._render());
|
||||
this._ro.observe(this);
|
||||
load();
|
||||
this._render();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
subs.delete(this._subFn);
|
||||
this.removeEventListener('dragenter', this);
|
||||
this.removeEventListener('dragover', this);
|
||||
this.removeEventListener('dragleave', this);
|
||||
this.removeEventListener('drop', this);
|
||||
if (this._ro) { this._ro.disconnect(); this._ro = null; }
|
||||
this._exitReframe(false);
|
||||
}
|
||||
|
||||
_enterReframe() {
|
||||
if (this.hasAttribute('data-reframe')) return;
|
||||
this.setAttribute('data-reframe', '');
|
||||
this._applyView();
|
||||
// Close on click outside (the spill handler stopPropagation()s so
|
||||
// in-image drags don't reach this) and on Escape. Listeners are held
|
||||
// on the instance so _exitReframe / disconnectedCallback can detach
|
||||
// exactly what was attached.
|
||||
this._outside = (e) => {
|
||||
if (e.composedPath && e.composedPath().includes(this)) return;
|
||||
this._exitReframe(true);
|
||||
};
|
||||
this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); };
|
||||
document.addEventListener('pointerdown', this._outside, true);
|
||||
document.addEventListener('keydown', this._esc, true);
|
||||
}
|
||||
|
||||
_exitReframe(commit) {
|
||||
if (!this.hasAttribute('data-reframe')) return;
|
||||
if (this._dragUp) this._dragUp();
|
||||
this.removeAttribute('data-reframe');
|
||||
this.removeAttribute('data-panning');
|
||||
if (this._outside) document.removeEventListener('pointerdown', this._outside, true);
|
||||
if (this._esc) document.removeEventListener('keydown', this._esc, true);
|
||||
this._outside = this._esc = null;
|
||||
if (commit) this._commitView();
|
||||
}
|
||||
|
||||
attributeChangedCallback() { if (this.shadowRoot) this._render(); }
|
||||
|
||||
// handleEvent — one listener object for all four drag events keeps the
|
||||
// add/remove symmetric and the depth counter correct.
|
||||
handleEvent(e) {
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
// Without preventDefault the browser never fires 'drop'.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
if (e.type === 'dragenter') this._depth++;
|
||||
this.setAttribute('data-over', '');
|
||||
} else if (e.type === 'dragleave') {
|
||||
// dragenter/leave fire for every descendant crossing — count depth
|
||||
// so hovering the icon inside the empty state doesn't flicker.
|
||||
if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); }
|
||||
} else if (e.type === 'drop') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._depth = 0;
|
||||
this.removeAttribute('data-over');
|
||||
const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
|
||||
if (f) this._ingest(f);
|
||||
}
|
||||
}
|
||||
|
||||
async _ingest(file) {
|
||||
this._setError(null);
|
||||
if (!file || ACCEPT.indexOf(file.type) < 0) {
|
||||
this._setError('Drop a PNG, JPEG, WebP, or AVIF image.');
|
||||
return;
|
||||
}
|
||||
// toDataUrl can take hundreds of ms on a large photo. A Clear or a
|
||||
// newer drop during that window would be clobbered when this await
|
||||
// resumes — bump + capture a generation so stale encodes bail.
|
||||
const gen = ++this._gen;
|
||||
try {
|
||||
const w = this.clientWidth || this.offsetWidth || MAX_DIM;
|
||||
const url = await toDataUrl(file, w);
|
||||
if (gen !== this._gen) return;
|
||||
// Only exit reframe once the new image is in hand — a rejected type
|
||||
// or decode failure leaves the in-progress crop untouched.
|
||||
this._exitReframe(false);
|
||||
const val = { u: url, s: 1, x: 0, y: 0 };
|
||||
setSlot(this.id || '', val);
|
||||
// Keep a session-local copy for id-less slots so the drop still
|
||||
// shows, even though it cannot persist.
|
||||
if (!this.id) { this._local = val; this._render(); }
|
||||
} catch (err) {
|
||||
if (gen !== this._gen) return;
|
||||
this._setError('Could not read that image.');
|
||||
console.warn('<image-slot> ingest failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
_setError(msg) {
|
||||
if (this._err) { this._err.remove(); this._err = null; }
|
||||
if (!msg) return;
|
||||
const d = document.createElement('div');
|
||||
d.className = 'err'; d.textContent = msg;
|
||||
this.shadowRoot.appendChild(d);
|
||||
this._err = d;
|
||||
setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000);
|
||||
}
|
||||
|
||||
// Reframing (pan/resize) is only meaningful for fit=cover — contain/fill
|
||||
// keep the old object-fit path and double-click is a no-op.
|
||||
_reframes() {
|
||||
return this.hasAttribute('data-filled') &&
|
||||
(this.getAttribute('fit') || 'cover') === 'cover';
|
||||
}
|
||||
|
||||
// Cover-baseline geometry, shared by clamp/apply/resize. Null until the
|
||||
// img has loaded (naturalWidth is 0 before that) or when the slot has no
|
||||
// layout box — ResizeObserver fires with a 0×0 rect under display:none,
|
||||
// and clamping against a degenerate 1×1 frame would silently pull the
|
||||
// stored pan toward zero.
|
||||
_geom() {
|
||||
const iw = this._img.naturalWidth, ih = this._img.naturalHeight;
|
||||
const fw = this.clientWidth, fh = this.clientHeight;
|
||||
if (!iw || !ih || !fw || !fh) return null;
|
||||
return { iw, ih, fw, fh, base: Math.max(fw / iw, fh / ih) };
|
||||
}
|
||||
|
||||
_clampView() {
|
||||
// Pan range on each axis is half the overflow past the frame edge.
|
||||
const g = this._geom();
|
||||
if (!g) return;
|
||||
const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50);
|
||||
const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50);
|
||||
this._view.x = Math.max(-mx, Math.min(mx, this._view.x));
|
||||
this._view.y = Math.max(-my, Math.min(my, this._view.y));
|
||||
}
|
||||
|
||||
_applyView() {
|
||||
const g = this._geom();
|
||||
const fit = this.getAttribute('fit') || 'cover';
|
||||
if (fit !== 'cover' || !g) {
|
||||
// Non-cover, or dimensions not known yet (before img load).
|
||||
this._img.style.width = '100%';
|
||||
this._img.style.height = '100%';
|
||||
this._img.style.left = '50%';
|
||||
this._img.style.top = '50%';
|
||||
this._img.style.objectFit = fit;
|
||||
this._img.style.objectPosition = this.getAttribute('position') || '50% 50%';
|
||||
return;
|
||||
}
|
||||
// Cover baseline: img fills the frame on its tighter axis at s=1, so
|
||||
// pan works immediately on the overflowing axis without zooming first.
|
||||
// Width/height and left/top are all frame-% — depends only on the
|
||||
// frame aspect ratio, so a responsive resize keeps the same crop. The
|
||||
// spill layer mirrors the same box so its corners = image corners.
|
||||
const k = g.base * this._view.s;
|
||||
const w = (g.iw * k / g.fw * 100) + '%';
|
||||
const h = (g.ih * k / g.fh * 100) + '%';
|
||||
const l = (50 + this._view.x) + '%';
|
||||
const t = (50 + this._view.y) + '%';
|
||||
this._img.style.width = w; this._img.style.height = h;
|
||||
this._img.style.left = l; this._img.style.top = t;
|
||||
this._img.style.objectFit = '';
|
||||
this._spill.style.width = w; this._spill.style.height = h;
|
||||
this._spill.style.left = l; this._spill.style.top = t;
|
||||
}
|
||||
|
||||
_commitView() {
|
||||
const v = { s: this._view.s, x: this._view.x, y: this._view.y };
|
||||
if (this._userUrl) v.u = this._userUrl;
|
||||
// Framing-only (no u) persists too so an author-src slot remembers its
|
||||
// crop; clearing the sidecar still falls through to src=.
|
||||
if (this.id) setSlot(this.id, v);
|
||||
else { this._local = v; }
|
||||
}
|
||||
|
||||
_render() {
|
||||
// Shape / mask. Presets use border-radius so the dashed ring can
|
||||
// follow the rounded outline; clip-path is only applied for an
|
||||
// explicit `mask` (the ring is hidden there since a rectangle
|
||||
// dashed border chopped by an arbitrary polygon looks broken).
|
||||
const mask = this.getAttribute('mask');
|
||||
const shape = (this.getAttribute('shape') || 'rounded').toLowerCase();
|
||||
let radius = '';
|
||||
if (shape === 'circle') radius = '50%';
|
||||
else if (shape === 'pill') radius = '9999px';
|
||||
else if (shape === 'rounded') {
|
||||
const n = parseFloat(this.getAttribute('radius'));
|
||||
radius = (Number.isFinite(n) ? n : 12) + 'px';
|
||||
}
|
||||
this._frame.style.borderRadius = mask ? '' : radius;
|
||||
this._frame.style.clipPath = mask || '';
|
||||
this._ring.style.borderRadius = mask ? '' : radius;
|
||||
this._ring.style.display = mask ? 'none' : '';
|
||||
|
||||
// Controls and reframe entry gate on this so share links stay read-only.
|
||||
const editable = !!(window.omelette && window.omelette.writeFile);
|
||||
this.toggleAttribute('data-editable', editable);
|
||||
this._sub.style.display = editable ? '' : 'none';
|
||||
|
||||
// Content. The sidecar is also writable by the agent's write_file
|
||||
// tool, so its value isn't guaranteed canvas-originated — only accept
|
||||
// data:image/ URLs from it. The `src` attribute is author-controlled
|
||||
// (Claude wrote it into the HTML) so it passes through unchanged.
|
||||
let stored = this.id ? getSlot(this.id) : this._local;
|
||||
if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null;
|
||||
const srcAttr = this.getAttribute('src') || '';
|
||||
this._userUrl = (stored && stored.u) || null;
|
||||
const url = this._userUrl || srcAttr;
|
||||
// Don't clobber an in-flight reframe with a store-triggered re-render.
|
||||
if (!this.hasAttribute('data-reframe')) {
|
||||
this._view = {
|
||||
s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1,
|
||||
x: stored && Number.isFinite(stored.x) ? stored.x : 0,
|
||||
y: stored && Number.isFinite(stored.y) ? stored.y : 0,
|
||||
};
|
||||
}
|
||||
this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image';
|
||||
// Toggle via style.display — the [hidden] attribute alone loses to
|
||||
// the display:flex / display:block rules in the stylesheet above.
|
||||
if (url) {
|
||||
if (this._img.getAttribute('src') !== url) {
|
||||
this._img.src = url;
|
||||
this._ghost.src = url;
|
||||
}
|
||||
this._img.style.display = 'block';
|
||||
this._empty.style.display = 'none';
|
||||
this.setAttribute('data-filled', '');
|
||||
this._clampView();
|
||||
this._applyView();
|
||||
} else {
|
||||
this._img.style.display = 'none';
|
||||
this._img.removeAttribute('src');
|
||||
this._ghost.removeAttribute('src');
|
||||
this._empty.style.display = 'flex';
|
||||
this.removeAttribute('data-filled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customElements.get('image-slot')) {
|
||||
customElements.define('image-slot', ImageSlot);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,239 @@
|
||||
/* LynkedUp Pro v3 — interactivity */
|
||||
|
||||
// ---------- Mobile nav (hamburger toggle) ----------
|
||||
(function() {
|
||||
const toggle = document.getElementById('navToggle');
|
||||
const links = document.getElementById('navLinks');
|
||||
const overlay = document.getElementById('navOverlay');
|
||||
if (!toggle || !links) return;
|
||||
|
||||
function setOpen(open) {
|
||||
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
toggle.classList.toggle('is-open', open);
|
||||
links.classList.toggle('is-open', open);
|
||||
if (overlay) overlay.classList.toggle('is-open', open);
|
||||
document.body.classList.toggle('nav-locked', open);
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const open = toggle.getAttribute('aria-expanded') !== 'true';
|
||||
setOpen(open);
|
||||
});
|
||||
if (overlay) overlay.addEventListener('click', () => setOpen(false));
|
||||
// Close on any link tap inside the panel
|
||||
links.querySelectorAll('a').forEach(a => {
|
||||
a.addEventListener('click', () => setOpen(false));
|
||||
});
|
||||
// Close when crossing back to desktop width
|
||||
const mq = window.matchMedia('(min-width: 1024px)');
|
||||
mq.addEventListener('change', e => { if (e.matches) setOpen(false); });
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
});
|
||||
})();
|
||||
|
||||
// ---------- Theme toggle (light/dark) ----------
|
||||
(function() {
|
||||
const root = document.documentElement;
|
||||
const key = 'lp-theme';
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
root.setAttribute('data-theme', saved);
|
||||
} else {
|
||||
root.setAttribute('data-theme', 'dark'); // default
|
||||
}
|
||||
const btn = document.getElementById('themeToggle');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
const current = root.getAttribute('data-theme') || 'dark';
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
root.setAttribute('data-theme', next);
|
||||
localStorage.setItem(key, next);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// ---------- Countdown timer (both final card + sticky bar) ----------
|
||||
(function() {
|
||||
let secs = 42, mins = 27, hours = 14, days = 3;
|
||||
const get = id => document.getElementById(id);
|
||||
const elFinal = { s: get('cdS'), m: get('cdM'), h: get('cdH'), d: get('cdD') };
|
||||
const elStk = { s: get('ssS'), m: get('ssM'), h: get('ssH'), d: get('ssD') };
|
||||
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
function render() {
|
||||
const v = { d: pad(days), h: pad(hours), m: pad(mins), s: pad(secs) };
|
||||
for (const k of ['d','h','m','s']) {
|
||||
if (elFinal[k]) elFinal[k].textContent = v[k];
|
||||
if (elStk[k]) elStk[k].textContent = v[k];
|
||||
}
|
||||
}
|
||||
setInterval(() => {
|
||||
secs--;
|
||||
if (secs < 0) { secs = 59; mins--;
|
||||
if (mins < 0) { mins = 59; hours--;
|
||||
if (hours < 0) { hours = 23; days = Math.max(0, days - 1); }
|
||||
}
|
||||
}
|
||||
render();
|
||||
}, 1000);
|
||||
render();
|
||||
})();
|
||||
|
||||
// ---------- Sticky CTA reveal/hide ----------
|
||||
(function() {
|
||||
const el = document.getElementById('stickyCta');
|
||||
const close = document.getElementById('stickyClose');
|
||||
if (!el || !close) return;
|
||||
let dismissed = false;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (dismissed) return;
|
||||
if (window.scrollY > 700) el.classList.add('show');
|
||||
else el.classList.remove('show');
|
||||
});
|
||||
close.addEventListener('click', () => {
|
||||
dismissed = true;
|
||||
el.classList.remove('show');
|
||||
document.body.style.paddingBottom = '0';
|
||||
});
|
||||
})();
|
||||
|
||||
// ---------- Video player ----------
|
||||
(function() {
|
||||
const frame = document.getElementById('videoFrame');
|
||||
const playBtn = document.getElementById('videoPlay');
|
||||
const videoEl = document.getElementById('videoEl');
|
||||
if (!frame || !playBtn || !videoEl) return;
|
||||
|
||||
function play(at) {
|
||||
frame.classList.add('playing');
|
||||
if (typeof at === 'number' && !isNaN(at)) {
|
||||
try { videoEl.currentTime = at; } catch (e) {}
|
||||
}
|
||||
videoEl.play().catch(() => {});
|
||||
}
|
||||
playBtn.addEventListener('click', () => play());
|
||||
document.querySelectorAll('.vc[data-seek]').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
const at = parseFloat(chip.dataset.seek);
|
||||
play(at);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// ---------- Dashboard product-view tabs ----------
|
||||
(function() {
|
||||
const wrap = document.getElementById('dashWindow');
|
||||
if (!wrap) return;
|
||||
const tabs = wrap.querySelectorAll('.dw-tab');
|
||||
const shots = wrap.querySelectorAll('.dw-shot');
|
||||
const urlText = document.getElementById('dwUrlText');
|
||||
|
||||
function activate(name) {
|
||||
tabs.forEach(t => {
|
||||
const on = t.dataset.tab === name;
|
||||
t.classList.toggle('is-active', on);
|
||||
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
if (on && urlText && t.dataset.url) urlText.textContent = t.dataset.url;
|
||||
});
|
||||
shots.forEach(s => s.classList.toggle('is-active', s.dataset.shot === name));
|
||||
}
|
||||
|
||||
tabs.forEach(t => {
|
||||
t.addEventListener('click', () => activate(t.dataset.tab));
|
||||
});
|
||||
})();
|
||||
|
||||
// ---------- Smooth in-page nav ----------
|
||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', (e) => {
|
||||
const id = a.getAttribute('href');
|
||||
if (id.length < 2) return;
|
||||
const t = document.querySelector(id);
|
||||
if (!t) return;
|
||||
e.preventDefault();
|
||||
window.scrollTo({ top: t.getBoundingClientRect().top + window.scrollY - 80, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- Tweaks panel ----------
|
||||
(function() {
|
||||
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
||||
"accentOrange": "#f97316",
|
||||
"accentCyan": "#22d3ee",
|
||||
"bgIntensity": 0.65,
|
||||
"showGrid": true
|
||||
}/*EDITMODE-END*/;
|
||||
|
||||
let state = { ...TWEAK_DEFAULTS };
|
||||
let active = false;
|
||||
const root = document.getElementById('tweaks-root');
|
||||
|
||||
function applyState() {
|
||||
document.documentElement.style.setProperty('--orange', state.accentOrange);
|
||||
document.documentElement.style.setProperty('--cyan', state.accentCyan);
|
||||
document.documentElement.style.setProperty('--cyan-2', state.accentCyan);
|
||||
const orbs = document.querySelectorAll('.bg-orb');
|
||||
orbs.forEach(o => o.style.opacity = state.bgIntensity);
|
||||
const grid = document.querySelector('.bg-grid');
|
||||
if (grid) grid.style.display = state.showGrid ? '' : 'none';
|
||||
}
|
||||
applyState();
|
||||
|
||||
function render() {
|
||||
if (!active) { root.innerHTML = ''; return; }
|
||||
root.innerHTML = `
|
||||
<div style="width:300px; background:rgba(5,15,36,.96); backdrop-filter:blur(20px); border:1px solid rgba(56,189,248,.3); border-radius:16px; box-shadow:0 30px 60px -20px rgba(0,0,0,.8); padding:18px; font-family:'Plus Jakarta Sans'; color:#e6edf6;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||
<strong style="font-size:14px; letter-spacing:-.01em; color:#fff;">Tweaks</strong>
|
||||
<button id="tw-close" style="border:0; background:transparent; cursor:pointer; color:#94a3b8; font-size:18px;">×</button>
|
||||
</div>
|
||||
|
||||
<div style="font-size:11px; color:#94a3b8; font-weight:600; text-transform:uppercase; letter-spacing:.05em; margin-bottom:8px;">CTA Orange</div>
|
||||
<div style="display:flex; gap:6px; margin-bottom:14px;">
|
||||
${['#f97316','#ef4444','#fbbf24','#ec4899'].map(c => `<button data-orange="${c}" style="flex:1; height:32px; border-radius:8px; border:2px solid ${state.accentOrange===c?'#fff':'transparent'}; background:${c}; cursor:pointer;"></button>`).join('')}
|
||||
</div>
|
||||
|
||||
<div style="font-size:11px; color:#94a3b8; font-weight:600; text-transform:uppercase; letter-spacing:.05em; margin-bottom:8px;">Accent Cyan</div>
|
||||
<div style="display:flex; gap:6px; margin-bottom:14px;">
|
||||
${['#22d3ee','#38bdf8','#a78bfa','#10b981'].map(c => `<button data-cyan="${c}" style="flex:1; height:32px; border-radius:8px; border:2px solid ${state.accentCyan===c?'#fff':'transparent'}; background:${c}; cursor:pointer;"></button>`).join('')}
|
||||
</div>
|
||||
|
||||
<div style="font-size:11px; color:#94a3b8; font-weight:600; text-transform:uppercase; letter-spacing:.05em; margin-bottom:8px;">Background glow · ${Math.round(state.bgIntensity*100)}%</div>
|
||||
<input type="range" min="0" max="1" step="0.05" value="${state.bgIntensity}" id="tw-bg" style="width:100%; margin-bottom:14px;">
|
||||
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:13px; color:#cbd5e1; cursor:pointer;">
|
||||
<input type="checkbox" id="tw-grid" ${state.showGrid?'checked':''}/>
|
||||
Show background grid
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
root.querySelector('#tw-close').onclick = () => {
|
||||
active = false; render();
|
||||
window.parent.postMessage({type:'__edit_mode_dismissed'}, '*');
|
||||
};
|
||||
root.querySelectorAll('[data-orange]').forEach(b => b.onclick = () => {
|
||||
state.accentOrange = b.dataset.orange; applyState(); render();
|
||||
window.parent.postMessage({type:'__edit_mode_set_keys', edits:{accentOrange: state.accentOrange}}, '*');
|
||||
});
|
||||
root.querySelectorAll('[data-cyan]').forEach(b => b.onclick = () => {
|
||||
state.accentCyan = b.dataset.cyan; applyState(); render();
|
||||
window.parent.postMessage({type:'__edit_mode_set_keys', edits:{accentCyan: state.accentCyan}}, '*');
|
||||
});
|
||||
root.querySelector('#tw-bg').oninput = (e) => {
|
||||
state.bgIntensity = parseFloat(e.target.value); applyState(); render();
|
||||
window.parent.postMessage({type:'__edit_mode_set_keys', edits:{bgIntensity: state.bgIntensity}}, '*');
|
||||
};
|
||||
root.querySelector('#tw-grid').onchange = (e) => {
|
||||
state.showGrid = e.target.checked; applyState(); render();
|
||||
window.parent.postMessage({type:'__edit_mode_set_keys', edits:{showGrid: state.showGrid}}, '*');
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data?.type === '__activate_edit_mode') { active = true; render(); }
|
||||
if (e.data?.type === '__deactivate_edit_mode') { active = false; render(); }
|
||||
});
|
||||
window.parent.postMessage({type:'__edit_mode_available'}, '*');
|
||||
})();
|
||||
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,797 @@
|
||||
/* global React, Reveal, GlowCard, Icon, Sparkline, DonutChart, BlueprintHouse, Counter */
|
||||
// Analytics, Field Intelligence, Testimonials, CTA, Footer
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
// ----------- Animated bar chart ------------
|
||||
function BarChart({ data, max, color = "var(--cyan)", height = 180 }) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "flex-end", gap: 8, height, padding: "12px 0" }}>
|
||||
{data.map((d, i) => {
|
||||
const h = (d.v / max) * (height - 30);
|
||||
return (
|
||||
<div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
|
||||
<div style={{
|
||||
width: "100%",
|
||||
height: h,
|
||||
background: d.highlight
|
||||
? `linear-gradient(180deg, var(--orange) 0%, rgba(255,138,30,0.3) 100%)`
|
||||
: `linear-gradient(180deg, ${color} 0%, ${color}33 100%)`,
|
||||
borderRadius: "6px 6px 2px 2px",
|
||||
boxShadow: d.highlight ? "0 0 18px rgba(255,138,30,0.7)" : `0 0 14px ${color}55`,
|
||||
animation: `growBar 1s ease-out ${i * 0.06}s both`,
|
||||
transformOrigin: "bottom",
|
||||
}}/>
|
||||
<span style={{ fontSize: 10, color: "var(--muted)" }} className="mono">{d.l}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- HUD ring widget --------------
|
||||
function HudRing({ value, label, color = "var(--cyan)", size = 140 }) {
|
||||
const r = (size - 18) / 2;
|
||||
const c = 2 * Math.PI * r;
|
||||
const dash = (value / 100) * c;
|
||||
return (
|
||||
<div style={{ position: "relative", width: size, height: size }}>
|
||||
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
|
||||
<circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth="6"/>
|
||||
<circle cx={size/2} cy={size/2} r={r} fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="6"
|
||||
strokeDasharray={`${dash} ${c}`}
|
||||
strokeLinecap="round"
|
||||
style={{ filter: `drop-shadow(0 0 8px ${color})`, transition: "stroke-dasharray 1s" }}
|
||||
/>
|
||||
{/* tick marks */}
|
||||
{Array.from({ length: 60 }, (_, i) => {
|
||||
const angle = (i / 60) * 2 * Math.PI;
|
||||
const inner = r + 4, outer = r + 10;
|
||||
const x1 = size/2 + inner * Math.cos(angle);
|
||||
const y1 = size/2 + inner * Math.sin(angle);
|
||||
const x2 = size/2 + outer * Math.cos(angle);
|
||||
const y2 = size/2 + outer * Math.sin(angle);
|
||||
return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke="rgba(0,209,255,0.18)" strokeWidth={i%5===0?"1.2":"0.5"}/>;
|
||||
})}
|
||||
</svg>
|
||||
<div style={{
|
||||
position: "absolute", inset: 0,
|
||||
display: "flex", flexDirection: "column",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
}}>
|
||||
<span className="tabular" style={{ fontFamily: "var(--font-display)", fontSize: 28, fontWeight: 600, color, lineHeight: 1, textShadow: `0 0 14px ${color}88` }}>
|
||||
<Counter to={value} suffix="%"/>
|
||||
</span>
|
||||
<span className="mono" style={{ fontSize: 9, color: "var(--muted)", letterSpacing: "0.18em", marginTop: 4 }}>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Analytics Section ------------
|
||||
function Analytics() {
|
||||
const bars = [
|
||||
{ l: "MON", v: 32 }, { l: "TUE", v: 48 }, { l: "WED", v: 38 },
|
||||
{ l: "THU", v: 62 }, { l: "FRI", v: 88, highlight: true },
|
||||
{ l: "SAT", v: 54 }, { l: "SUN", v: 42 },
|
||||
];
|
||||
return (
|
||||
<section className="section" id="analytics">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 56, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§08</span>
|
||||
<span>ANALYTICS & CONTROL CENTER</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22 }}>
|
||||
Measure once.<br/>
|
||||
<span className="accent">Use everywhere.</span>
|
||||
</h2>
|
||||
<p className="sub" style={{ marginTop: 18 }}>
|
||||
A cinematic, HUD-style enterprise dashboard built from your live
|
||||
operational data, calibrated for executive decisions and field reality.
|
||||
</p>
|
||||
</div>
|
||||
<div className="row gap-12">
|
||||
<span className="dim-tag">// LIVE STREAM</span>
|
||||
<span className="dim-tag" style={{ color: "var(--green)", borderColor: "rgba(43,212,156,0.4)" }}>● 142 MS</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={1}>
|
||||
<GlowCard style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div style={{
|
||||
padding: "20px 26px",
|
||||
borderBottom: "1px solid var(--border)",
|
||||
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||
background: "rgba(0,209,255,0.04)",
|
||||
}}>
|
||||
<div className="row gap-16">
|
||||
<span className="mono" style={{ fontSize: 11, color: "var(--cyan-2)", letterSpacing: "0.22em" }}>OWNERS BOX · ANALYTICS</span>
|
||||
<span className="chip green"><span className="live-dot"/> LIVE STREAM</span>
|
||||
</div>
|
||||
<div className="row gap-12" style={{ fontSize: 11, color: "var(--muted)" }} >
|
||||
<span>Last sync: 142 ms ago</span>
|
||||
<span>·</span>
|
||||
<span className="mono" style={{ color: "var(--cyan)" }}>Q2 · 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 0 }}>
|
||||
{/* Left main panel */}
|
||||
<div style={{ padding: 28, borderRight: "1px solid var(--border)" }}>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 8 }}>
|
||||
<div>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--muted)", letterSpacing: "0.18em" }}>WEEKLY REVENUE</div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontSize: 40, fontWeight: 600, color: "var(--cyan)", textShadow: "0 0 20px rgba(0,209,255,0.5)" }}>
|
||||
$<Counter to={200000} duration={2200}/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end" }}>
|
||||
<span className="chip orange" style={{ fontSize: 10 }}>▲ +28.4% wow</span>
|
||||
<span style={{ fontSize: 11, color: "var(--muted)", marginTop: 6 }}>vs. $155,763 last week</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BarChart data={bars} max={100} height={220}/>
|
||||
|
||||
<div className="row" style={{ gap: 14, marginTop: 14 }}>
|
||||
{[
|
||||
{ label: "GROSS MARGIN", v: 42, color: "var(--cyan)" },
|
||||
{ label: "CYCLE TIME", v: 73, color: "var(--orange)" },
|
||||
{ label: "CREW UTIL.", v: 87, color: "var(--green)" },
|
||||
{ label: "PIPELINE HEALTH", v: 94, color: "var(--violet)" },
|
||||
].map(s => (
|
||||
<div key={s.label} style={{ flex: 1 }}>
|
||||
<div className="mono" style={{ fontSize: 9, color: "var(--muted)", letterSpacing: "0.14em" }}>{s.label}</div>
|
||||
<div className="tabular" style={{ fontFamily: "var(--font-display)", fontSize: 22, fontWeight: 600, color: s.color, lineHeight: 1.1 }}>
|
||||
<Counter to={s.v} suffix="%"/>
|
||||
</div>
|
||||
<div style={{ height: 3, background: "rgba(255,255,255,0.06)", borderRadius: 2, marginTop: 6, overflow: "hidden" }}>
|
||||
<div style={{
|
||||
width: `${s.v}%`, height: "100%",
|
||||
background: s.color,
|
||||
boxShadow: `0 0 8px ${s.color}`,
|
||||
animation: "growW 1.2s ease-out",
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right HUD column */}
|
||||
<div style={{ padding: 28, display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<div className="row" style={{ justifyContent: "space-around" }}>
|
||||
<HudRing value={92} label="OPS HEALTH" color="var(--cyan)" size={130}/>
|
||||
<HudRing value={78} label="FORECAST" color="var(--orange)" size={130}/>
|
||||
</div>
|
||||
|
||||
{/* Forecast list */}
|
||||
<div style={{ padding: 16, border: "1px solid var(--border)", borderRadius: 12, background: "rgba(0,209,255,0.03)" }}>
|
||||
<div className="row" style={{ justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: "0.16em" }}>AI FORECAST · NEXT 14 DAYS</span>
|
||||
<Icon name="spark" size={14} stroke="var(--cyan)"/>
|
||||
</div>
|
||||
{[
|
||||
{ l: "Estimated revenue", v: "$430K", c: "var(--cyan)" },
|
||||
{ l: "Crews needed", v: "14", c: "var(--orange)" },
|
||||
{ l: "Weather risk", v: "Low", c: "var(--green)" },
|
||||
{ l: "Material orders", v: "23", c: "var(--violet)" },
|
||||
].map((f, i) => (
|
||||
<div key={i} className="row" style={{ justifyContent: "space-between", padding: "8px 0", borderTop: i ? "1px solid rgba(255,255,255,0.05)" : "none" }}>
|
||||
<span style={{ fontSize: 12, color: "var(--ink-2)" }}>{f.l}</span>
|
||||
<span className="tabular" style={{ fontFamily: "var(--font-display)", fontSize: 15, fontWeight: 600, color: f.c }}>{f.v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mini activity */}
|
||||
<div style={{ padding: 16, border: "1px solid var(--border)", borderRadius: 12, background: "rgba(0,209,255,0.03)" }}>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--muted)", letterSpacing: "0.16em", display: "block", marginBottom: 10 }}>LIVE FEED</span>
|
||||
{[
|
||||
{ c: "var(--green)", t: "Payment received", s: "$8,250 · Maple Project" },
|
||||
{ c: "var(--cyan)", t: "Estimate accepted", s: "Pine Rd · $14,800" },
|
||||
{ c: "var(--orange)", t: "Crew dispatched", s: "Diego's crew → 456 Oak" },
|
||||
].map((a, i) => (
|
||||
<div key={i} className="row gap-12" style={{ padding: "6px 0" }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: a.c, boxShadow: `0 0 8px ${a.c}` }}/>
|
||||
<div style={{ flex: 1, fontSize: 12 }}>
|
||||
<span style={{ fontWeight: 600 }}>{a.t}</span>{" "}
|
||||
<span style={{ color: "var(--muted)" }}>· {a.s}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlowCard>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Field Intelligence Section ----------
|
||||
function FieldIntelligence() {
|
||||
return (
|
||||
<section className="section" id="intelligence">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 56, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§09</span>
|
||||
<span>FIELD INTELLIGENCE</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22, maxWidth: 880 }}>
|
||||
Field intelligence.<br/>
|
||||
<span className="accent">Not just CRM.</span>
|
||||
</h2>
|
||||
<p className="sub" style={{ marginTop: 18 }}>
|
||||
AI-powered measurements, photo evidence, claim-ready reports,
|
||||
and on-site decisions in minutes, not days.
|
||||
</p>
|
||||
</div>
|
||||
<div className="row gap-12">
|
||||
<span className="dim-tag">// AI & LIDAR</span>
|
||||
<span className="dim-tag" style={{ color: "var(--orange-2)", borderColor: "rgba(255,138,30,0.4)" }}>// PRECISION MODE</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Big wireframe stage */}
|
||||
<Reveal delay={1}>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
height: 620,
|
||||
background: `
|
||||
radial-gradient(900px 500px at 50% 50%, rgba(0,209,255,0.18), transparent 70%),
|
||||
linear-gradient(180deg, rgba(2,6,14,0.4), rgba(2,6,14,0.2))
|
||||
`,
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{/* Stage floor grid */}
|
||||
<div style={{
|
||||
position: "absolute", inset: 0,
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(0,209,255,0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,209,255,0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: "40px 40px",
|
||||
maskImage: "radial-gradient(ellipse 80% 60% at 50% 60%, #000 30%, transparent 80%)",
|
||||
}}/>
|
||||
|
||||
{/* Huge centered wireframe house */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: "55%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}>
|
||||
<BlueprintHouse size={780} withMeasurements={true} withScan={true}/>
|
||||
</div>
|
||||
|
||||
{/* Floating hex logo above */}
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
left: "50%", top: "30%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
animation: "floatY 4s ease-in-out infinite",
|
||||
}}>
|
||||
<img src="/uploads/lynkedup-logo.png" alt="LynkedUp Pro"
|
||||
width="140" height="140"
|
||||
style={{ width: 140, height: 140, objectFit: "contain", filter: "drop-shadow(0 0 28px rgba(0,209,255,0.9))" }}/>
|
||||
</div>
|
||||
|
||||
{/* Side data overlays */}
|
||||
<div style={{
|
||||
position: "absolute", top: 40, left: 40,
|
||||
padding: 16, width: 240,
|
||||
background: "rgba(0,209,255,0.07)",
|
||||
border: "1px solid rgba(0,209,255,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(10px)",
|
||||
boxShadow: "0 0 30px -10px rgba(0,209,255,0.6)",
|
||||
}}>
|
||||
<div className="mono" style={{ fontSize: 10, color: "var(--cyan-2)", letterSpacing: "0.18em", marginBottom: 8 }}>AI MEASUREMENT</div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontSize: 28, fontWeight: 600, color: "var(--cyan)", lineHeight: 1, textShadow: "0 0 14px rgba(0,209,255,0.6)" }}>
|
||||
<Counter to={2347.18} decimals={2}/> <span style={{ fontSize: 12 }}>SQ FT</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>Roof area · 14 planes detected</div>
|
||||
<div style={{ marginTop: 10, padding: "8px 10px", background: "rgba(0,209,255,0.07)", borderRadius: 6, fontSize: 11, color: "var(--cyan-2)" }} className="mono">
|
||||
● SCAN COMPLETE · 24 PHOTOS
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "absolute", top: 40, right: 40,
|
||||
padding: 16, width: 220,
|
||||
background: "rgba(255,138,30,0.07)",
|
||||
border: "1px solid rgba(255,138,30,0.45)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(10px)",
|
||||
boxShadow: "0 0 30px -10px rgba(255,138,30,0.5)",
|
||||
}}>
|
||||
<div className="row gap-8" style={{ marginBottom: 8 }}>
|
||||
<Icon name="brain" size={14} stroke="var(--orange)"/>
|
||||
<span className="mono" style={{ fontSize: 10, color: "var(--orange-2)", letterSpacing: "0.18em" }}>AI-POWERED</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 17 }}>Precision Mode</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>AI & LiDAR active</div>
|
||||
<div style={{ marginTop: 10, display: "flex", justifyContent: "space-between", fontSize: 11 }}>
|
||||
<span style={{ color: "var(--muted)" }}>Confidence</span>
|
||||
<span style={{ color: "var(--orange)", fontWeight: 600 }} className="tabular">99.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "absolute", bottom: 40, left: 40,
|
||||
padding: 14, width: 260,
|
||||
background: "rgba(43,212,156,0.07)",
|
||||
border: "1px solid rgba(43,212,156,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}>
|
||||
<div className="mono" style={{ fontSize: 10, color: "var(--green)", letterSpacing: "0.16em", marginBottom: 6 }}>CLAIM READY</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600 }}>Xactimate®-style report</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>Photo evidence attached · Generated in 6m 42s</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
position: "absolute", bottom: 40, right: 40,
|
||||
padding: 14, width: 220,
|
||||
background: "rgba(138,107,255,0.07)",
|
||||
border: "1px solid rgba(138,107,255,0.4)",
|
||||
borderRadius: 12,
|
||||
backdropFilter: "blur(10px)",
|
||||
}}>
|
||||
<div className="mono" style={{ fontSize: 10, color: "var(--violet)", letterSpacing: "0.16em", marginBottom: 6 }}>PLANES DETECTED</div>
|
||||
<div className="tabular" style={{ fontFamily: "var(--font-display)", fontSize: 28, fontWeight: 600, color: "var(--violet)" }}>
|
||||
<Counter to={14}/>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>Largest: 425.32 sq ft</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* Intelligence sub-cards */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 16, marginTop: 32 }}>
|
||||
{[
|
||||
{ v: "10×", label: "FASTER", desc: "Measure in minutes, not hours.", color: "var(--cyan)", icon: "clock" },
|
||||
{ v: "98.6%", label: "ACCURATE", desc: "AI & LiDAR precision you can rely on.", color: "var(--orange)", icon: "target" },
|
||||
{ v: "6m 42s", label: "CLAIM READY", desc: "Evidence-backed reports that get paid.", color: "var(--green)", icon: "shield" },
|
||||
{ v: "∞", label: "CLOUD SYNC", desc: "Real-time access across your team.", color: "var(--violet)", icon: "sync" },
|
||||
].map((c, i) => (
|
||||
<Reveal key={c.label} delay={i + 1}>
|
||||
<GlowCard>
|
||||
<div className="pad">
|
||||
<Icon name={c.icon} size={22} stroke={c.color}/>
|
||||
<div className="tabular" style={{ fontFamily: "var(--font-display)", fontSize: 36, fontWeight: 600, color: c.color, lineHeight: 1, marginTop: 16, textShadow: `0 0 14px ${c.color}55` }}>{c.v}</div>
|
||||
<div className="mono" style={{ fontSize: 10, letterSpacing: "0.2em", marginTop: 6 }}>{c.label}</div>
|
||||
<div style={{ fontSize: 12, color: "var(--muted)", marginTop: 6 }}>{c.desc}</div>
|
||||
</div>
|
||||
</GlowCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Testimonials -----------
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
quote: "LynkedUp Pro turned our chaos into a clear, connected operation. We close jobs faster and our margins are visible for the first time.",
|
||||
name: "Maya Patel",
|
||||
title: "Operations Manager · Plano, TX",
|
||||
init: "MP",
|
||||
color: "var(--cyan)",
|
||||
metric: "+42% revenue",
|
||||
},
|
||||
{
|
||||
quote: "It's the closest thing to having an extra ops director in the field. The AI Copilot has saved us from at least a dozen costly mistakes.",
|
||||
name: "Daniel Cruz",
|
||||
title: "Founder · Houston, TX",
|
||||
init: "DC",
|
||||
color: "var(--orange)",
|
||||
metric: "−68% admin time",
|
||||
},
|
||||
{
|
||||
quote: "Field teams adopted it in a week. Reports that took our office two days now take 4-5 minutes. It is genuinely transformational software.",
|
||||
name: "Ashley Reed",
|
||||
title: "Operations Manager · Shreveport, LA",
|
||||
init: "AR",
|
||||
color: "var(--green)",
|
||||
metric: "10K+ jobs synced",
|
||||
},
|
||||
];
|
||||
|
||||
function Testimonials() {
|
||||
return (
|
||||
<section className="section" id="customers">
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 56, flexWrap: "wrap", gap: 16 }}>
|
||||
<div>
|
||||
<span className="section-number">
|
||||
<span className="num">§10</span>
|
||||
<span>CUSTOMER VOICES</span>
|
||||
</span>
|
||||
<h2 className="h-section" style={{ marginTop: 22 }}>
|
||||
Operators are betting on <span className="accent">LynkedUp Pro</span>.
|
||||
</h2>
|
||||
</div>
|
||||
<span className="dim-tag" style={{ height: 30, padding: "4px 12px" }}>// 3,000+ TEAMS</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 22 }}>
|
||||
{TESTIMONIALS.map((t, i) => (
|
||||
<Reveal key={t.name} delay={i + 1}>
|
||||
<GlowCard style={{ height: "100%" }}>
|
||||
<div className="pad" style={{ display: "flex", flexDirection: "column", height: "100%", gap: 20 }}>
|
||||
<div style={{ fontSize: 36, fontFamily: "var(--font-display)", color: t.color, lineHeight: 0.5, opacity: 0.7 }}>“</div>
|
||||
<p style={{ fontSize: 15, lineHeight: 1.55, color: "var(--ink)", margin: 0, flex: 1 }}>{t.quote}</p>
|
||||
<div className="row gap-12" style={{ justifyContent: "space-between" }}>
|
||||
<div className="row gap-12">
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: "50%",
|
||||
background: `linear-gradient(135deg, ${t.color}, rgba(255,255,255,0.05))`,
|
||||
border: `1px solid ${t.color}99`,
|
||||
display: "grid", placeItems: "center",
|
||||
fontFamily: "var(--font-display)",
|
||||
fontWeight: 600, fontSize: 13,
|
||||
color: "white",
|
||||
boxShadow: `0 0 18px -6px ${t.color}`,
|
||||
}}>{t.init}</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: "var(--font-display)", fontSize: 14, fontWeight: 600 }}>{t.name}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--muted)" }}>{t.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="chip" style={{ borderColor: `${t.color}55`, color: t.color, background: `${t.color}11` }}>{t.metric}</span>
|
||||
</div>
|
||||
</div>
|
||||
</GlowCard>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Final CTA -------------
|
||||
function FinalCTA() {
|
||||
return (
|
||||
<section className="section" id="demo" style={{ paddingTop: 100, paddingBottom: 100 }}>
|
||||
<div className="beam top"/>
|
||||
<div className="shell">
|
||||
<Reveal>
|
||||
<div style={{
|
||||
position: "relative",
|
||||
padding: "100px 60px",
|
||||
borderRadius: 32,
|
||||
border: "1px solid var(--border-strong)",
|
||||
background: `
|
||||
radial-gradient(700px 400px at 20% 0%, rgba(0,209,255,0.25), transparent 60%),
|
||||
radial-gradient(700px 400px at 80% 100%, rgba(255,138,30,0.18), transparent 65%),
|
||||
linear-gradient(180deg, rgba(4,12,28,0.9), rgba(2,6,14,0.95))
|
||||
`,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 60px 120px -40px rgba(0,209,255,0.55)",
|
||||
}}>
|
||||
{/* Floor grid */}
|
||||
<div style={{
|
||||
position: "absolute", inset: 0,
|
||||
backgroundImage: `linear-gradient(rgba(0,209,255,0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(0,209,255,0.07) 1px, transparent 1px)`,
|
||||
backgroundSize: "44px 44px",
|
||||
maskImage: "radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)",
|
||||
}}/>
|
||||
|
||||
{/* Floating light beams */}
|
||||
<div style={{
|
||||
position: "absolute", left: "10%", top: 0, bottom: 0, width: 2,
|
||||
background: "linear-gradient(180deg, transparent, var(--cyan), transparent)",
|
||||
filter: "blur(2px)", opacity: 0.5, animation: "floatY 3s ease-in-out infinite",
|
||||
}}/>
|
||||
<div style={{
|
||||
position: "absolute", right: "12%", top: 0, bottom: 0, width: 2,
|
||||
background: "linear-gradient(180deg, transparent, var(--orange), transparent)",
|
||||
filter: "blur(2px)", opacity: 0.4, animation: "floatY 4s ease-in-out infinite", animationDelay: "1s",
|
||||
}}/>
|
||||
|
||||
<div style={{ position: "relative", zIndex: 2, textAlign: "center", maxWidth: 880, margin: "0 auto" }}>
|
||||
<span className="eyebrow"><span className="dot"/> READY WHEN YOU ARE</span>
|
||||
<h2 className="h-display" style={{ marginTop: 26, fontSize: "clamp(54px, 7.5vw, 104px)" }}>
|
||||
<span className="grad">Run construction</span><br/>
|
||||
<span className="underline">without chaos.</span>
|
||||
</h2>
|
||||
<p className="sub" style={{ margin: "26px auto 40px" }}>
|
||||
Join the operators replacing five tools, four spreadsheets, and a
|
||||
whiteboard with one AI-powered command center built for the
|
||||
modern construction enterprise.
|
||||
</p>
|
||||
<div className="row gap-16" style={{ justifyContent: "center" }}>
|
||||
<a href="https://book.lynkeduppro.com/" className="btn btn-primary" style={{ height: 60, padding: "0 36px", fontSize: 16 }}>
|
||||
Book a demo <span className="arrow">→</span>
|
||||
</a>
|
||||
<a href="https://book.lynkeduppro.com/" className="btn btn-orange" style={{ height: 60, padding: "0 36px", fontSize: 16 }}>
|
||||
Claim Founders Lifetime Deal
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="row gap-24" style={{ justifyContent: "center", marginTop: 40, color: "var(--muted)", fontSize: 12 }}>
|
||||
<span className="row gap-8"><span className="live-dot"/> No credit card</span>
|
||||
<span>·</span>
|
||||
<span>SOC 2 Type II</span>
|
||||
<span>·</span>
|
||||
<span>White-glove onboarding</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Footer ---------------
|
||||
function Footer() {
|
||||
const cols = [
|
||||
{ h: "Product", items: ["Platform overview", "AI Copilot", "Mobile app", "Pricing", "Changelog"] },
|
||||
{ h: "Solutions", items: ["Roofing", "General contractors", "Infrastructure", "Enterprise", "Field teams"] },
|
||||
{ h: "Integrations", items: ["QuickBooks", "Stripe", "Calendly", "iOS / Google", "Zapier"] },
|
||||
{ h: "Resources", items: ["Documentation", "Customer stories", "Security", "Status", "API"] },
|
||||
{ h: "Contact", items: ["Talk to sales", "Support", "Careers", "Partner program", "Press"] },
|
||||
];
|
||||
return (
|
||||
<footer style={{
|
||||
position: "relative",
|
||||
padding: "80px 0 40px",
|
||||
borderTop: "1px solid var(--border)",
|
||||
background: "linear-gradient(180deg, rgba(0,209,255,0.04), transparent)",
|
||||
}}>
|
||||
<div className="shell">
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1.4fr repeat(5, 1fr)", gap: 40, marginBottom: 56 }}>
|
||||
<div>
|
||||
<Logo size={36}/>
|
||||
<p style={{ marginTop: 16, fontSize: 14, color: "var(--muted)", maxWidth: 280, lineHeight: 1.55 }}>
|
||||
AI construction operating system built for field teams, modern
|
||||
contractors and infrastructure companies. Built for roofing.
|
||||
Driven by innovation.
|
||||
</p>
|
||||
<div className="row gap-12" style={{ marginTop: 22 }}>
|
||||
{["X","in","gh","yt"].map(s => (
|
||||
<a key={s} href="https://book.lynkeduppro.com/" style={{
|
||||
width: 36, height: 36, borderRadius: 8,
|
||||
border: "1px solid var(--border)",
|
||||
display: "grid", placeItems: "center",
|
||||
fontFamily: "var(--font-mono)", fontSize: 11,
|
||||
color: "var(--ink-2)", textDecoration: "none",
|
||||
background: "rgba(0,209,255,0.04)",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--cyan)"; e.currentTarget.style.color = "var(--cyan)"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--border)"; e.currentTarget.style.color = "var(--ink-2)"; }}
|
||||
>{s}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{cols.map(c => (
|
||||
<div key={c.h}>
|
||||
<div className="mono" style={{ fontSize: 11, color: "var(--cyan-2)", letterSpacing: "0.2em", marginBottom: 16 }}>{c.h.toUpperCase()}</div>
|
||||
<ul style={{ listStyle: "none", padding: 0, margin: 0, display: "flex", flexDirection: "column", gap: 10 }}>
|
||||
{c.items.map(it => (
|
||||
<li key={it}><a href="https://book.lynkeduppro.com/" style={{ fontSize: 13, color: "var(--ink-2)", textDecoration: "none", transition: "color 0.2s" }}
|
||||
onMouseEnter={e => e.currentTarget.style.color = "var(--cyan)"}
|
||||
onMouseLeave={e => e.currentTarget.style.color = "var(--ink-2)"}
|
||||
>{it}</a></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "linear-gradient(90deg, transparent, var(--border-strong), transparent)" }}/>
|
||||
|
||||
<div className="row" style={{ justifyContent: "space-between", marginTop: 24, fontSize: 12, color: "var(--muted)" }}>
|
||||
<span>© 2026 LynkedUp Pro · Built for roofing. Built for you.</span>
|
||||
<div className="row gap-24">
|
||||
<a href="https://book.lynkeduppro.com/" style={{ color: "inherit", textDecoration: "none" }}>Privacy</a>
|
||||
<a href="https://book.lynkeduppro.com/" style={{ color: "inherit", textDecoration: "none" }}>Terms</a>
|
||||
<a href="https://book.lynkeduppro.com/" style={{ color: "inherit", textDecoration: "none" }}>Security</a>
|
||||
<span className="row gap-8"><span className="live-dot"/> All systems normal</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- Pricing / Lifetime Offer ------------
|
||||
function Pricing() {
|
||||
return (
|
||||
<section className="roofing-offer-section" id="pricing">
|
||||
<div className="container">
|
||||
|
||||
<div className="text-center roofing-heading mb-5">
|
||||
<span className="offer-badge">🔥 Limited Founder Offer</span>
|
||||
<h2>Own Your Roofing CRM. Pay Once.</h2>
|
||||
<p>Stop paying monthly for multiple disconnected tools. Get founders lifetime access
|
||||
to LynkedUpPro and scale your roofing business with one intelligent platform.</p>
|
||||
</div>
|
||||
|
||||
<div className="row g-4 align-items-stretch">
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="lifetime-card h-100">
|
||||
<div className="popular-tag">MOST POPULAR</div>
|
||||
<h3>Founders Lifetime Access</h3>
|
||||
<p className="small-text">One payment. Unlimited operational control.</p>
|
||||
<div className="price-box">
|
||||
<div className="price-anchor">
|
||||
<span className="price-was">$25,000</span>
|
||||
<span className="price-save">Save $23,000 · 92% off</span>
|
||||
</div>
|
||||
<h2>$2000</h2>
|
||||
<span>PER LICENSE • PAY ONCE • USE FOREVER</span>
|
||||
</div>
|
||||
<ul className="feature-list">
|
||||
<li>Complete Roofing CRM</li>
|
||||
<li>Lead Management System</li>
|
||||
<li>Canvassing & Field Tracking</li>
|
||||
<li>Insurance Workflow</li>
|
||||
<li>Proposal & Inspection Tools</li>
|
||||
<li>Team Management Dashboard</li>
|
||||
<li>Unlimited Projects</li>
|
||||
<li>Future Feature Updates</li>
|
||||
<li>Priority Support</li>
|
||||
</ul>
|
||||
<a href="https://book.lynkeduppro.com/" className="deal-btn">Claim Founders Lifetime Deal</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-6">
|
||||
<div className="roofing-info-card h-100">
|
||||
<span className="mini-badge">Why Roofers Choose LynkedUpPro</span>
|
||||
<h3>Replace 5 Different Tools With One Roofing OS</h3>
|
||||
<p className="info-para">Most roofing companies spend thousands yearly on disconnected
|
||||
CRMs, canvassing apps, spreadsheets, proposal software, and field coordination tools.</p>
|
||||
<div className="info-box">
|
||||
<h5>More Revenue Visibility</h5>
|
||||
<p>Track leads, conversions, and team performance from one dashboard.</p>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<h5>Less Operational Chaos</h5>
|
||||
<p>Eliminate scattered communication and disconnected workflows.</p>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<h5>Built for Roofing Teams</h5>
|
||||
<p>Purpose-built for storm restoration, canvassing, insurance, and roofing operations.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="terms-wrapper mt-4">
|
||||
<details className="custom-accordion-item">
|
||||
<summary className="accordion-button">Deal Terms & Conditions</summary>
|
||||
<div className="accordion-body">
|
||||
<div className="deal-terms-legal" style={{ fontSize: 12, lineHeight: 1.65, color: "#444" }}>
|
||||
<p>All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Claims may be based on a combination of internal testing, platform development data, field-tested data, and feedback from founding users. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.</p>
|
||||
<p>Some user names, company names, examples, or identifying details may have been changed, modified, or withheld to protect user privacy, confidential business information, proprietary workflows, and trade secrets.</p>
|
||||
<p>The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. The Founders License includes access to core CRM features and the platform features made available under the applicable license terms. The Lifetime Founders License does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.</p>
|
||||
<p>SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges. These charges may be billed separately based on actual usage, service level, selected features, or third-party provider costs.</p>
|
||||
<p>Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, premium automation, and other enhanced modules, may require additional fees, setup costs, subscriptions, or usage-based billing.</p>
|
||||
<p style={{ marginBottom: 0 }}>Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="roofing-cta-box mt-5 text-center">
|
||||
<h2>Roofing Moves Fast. Your CRM Should Too.</h2>
|
||||
<p>Join the next generation of roofing companies scaling with automation,
|
||||
field intelligence, and operational visibility.</p>
|
||||
<div className="cta-btn-group">
|
||||
<a href="https://book.lynkeduppro.com/" className="start-btn">Start Founders Lifetime Access</a>
|
||||
<a href="https://book.lynkeduppro.com/" className="sales-btn">Book a demo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.roofing-offer-section{
|
||||
padding:64px 0;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 35%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 35%),
|
||||
#1a120d;
|
||||
overflow:hidden; position:relative;
|
||||
}
|
||||
.roofing-offer-section .container{max-width:1120px;margin:0 auto;padding:0 20px;}
|
||||
.roofing-offer-section .row{display:flex;flex-wrap:wrap;margin:0 -12px;}
|
||||
.roofing-offer-section .row.g-4{margin:-12px;}
|
||||
.roofing-offer-section .row.g-4 > [class*="col-"]{padding:12px;}
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 50%;max-width:50%;padding:0 12px;}
|
||||
.roofing-offer-section .align-items-stretch{align-items:stretch;}
|
||||
.roofing-offer-section .h-100{height:100%;}
|
||||
.roofing-offer-section .text-center{text-align:center;}
|
||||
.roofing-offer-section .mb-5{margin-bottom:36px;}
|
||||
.roofing-offer-section .mt-4{margin-top:20px;}
|
||||
.roofing-offer-section .mt-5{margin-top:36px;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:38px;font-weight:800;line-height:1.15;color:#fff;margin-top:14px;}
|
||||
.roofing-offer-section .roofing-heading p{color:#d7d7d7;max-width:620px;margin:12px auto 0;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .offer-badge{display:inline-block;padding:6px 14px;border-radius:40px;background:rgba(255,255,255,0.08);color:#fff;font-size:12px;font-weight:600;}
|
||||
.roofing-offer-section .lifetime-card{position:relative;background:linear-gradient(180deg,#12121b,#0d0d14);border-radius:20px;padding:28px;border:1px solid rgba(255,255,255,0.08);overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,0.35);}
|
||||
.roofing-offer-section .popular-tag{position:absolute;top:16px;right:-45px;background:linear-gradient(90deg,#ff8b1e,#53c7ff);color:#fff;font-size:10px;font-weight:700;padding:6px 50px;transform:rotate(40deg);}
|
||||
.roofing-offer-section .lifetime-card h3{color:#fff;font-size:22px;font-weight:700;margin:0;}
|
||||
.roofing-offer-section .small-text{color:#b7b7b7;font-size:13.5px;margin:4px 0 18px;}
|
||||
.roofing-offer-section .price-box h2{color:#fff;font-size:48px;font-weight:800;line-height:1;margin-bottom:4px;}
|
||||
.roofing-offer-section .price-box span{color:#8d8d8d;font-size:12px;letter-spacing:1px;}
|
||||
.roofing-offer-section .price-anchor{display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .price-was{position:relative;color:#c98f6a;font-size:22px;font-weight:700;text-decoration:line-through;text-decoration-color:rgba(255,147,33,.75);text-decoration-thickness:2px;}
|
||||
.roofing-offer-section .price-save{display:inline-block;padding:4px 10px;border-radius:999px;background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#0d0d0f;font-size:11px;font-weight:800;letter-spacing:.02em;white-space:nowrap;}
|
||||
.roofing-offer-section .feature-list{padding:0;margin:24px 0;list-style:none;}
|
||||
.roofing-offer-section .feature-list li{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.05);border-radius:11px;padding:10px 14px;margin-bottom:9px;color:#fff;font-size:13.5px;position:relative;padding-left:42px;}
|
||||
.roofing-offer-section .feature-list li::before{content:"✓";position:absolute;left:14px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:11px;}
|
||||
.roofing-offer-section .deal-btn{display:inline-block;padding:12px 24px;border-radius:12px;text-decoration:none;color:#fff;font-size:14px;font-weight:700;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,0.3);}
|
||||
.roofing-offer-section .roofing-info-card{border-radius:20px;padding:28px;background:linear-gradient(180deg,rgba(255,255,255,0.12),rgba(255,255,255,0.03)),linear-gradient(180deg,#d6892d,#0d99f1);backdrop-filter:blur(10px);color:#fff;}
|
||||
.roofing-offer-section .mini-badge{display:inline-block;background:rgba(255,255,255,0.2);padding:6px 14px;border-radius:40px;font-size:12px;font-weight:600;margin-bottom:16px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:26px;font-weight:800;line-height:1.2;margin-bottom:16px;}
|
||||
.roofing-offer-section .info-para{color:rgba(255,255,255,0.85);font-size:14px;line-height:1.55;margin-bottom:20px;}
|
||||
.roofing-offer-section .info-box{background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px;margin-bottom:12px;backdrop-filter:blur(8px);}
|
||||
.roofing-offer-section .info-box h5{font-size:16px;font-weight:700;margin:0 0 5px;}
|
||||
.roofing-offer-section .info-box p{margin:0;font-size:13.5px;line-height:1.5;color:rgba(255,255,255,0.9);}
|
||||
.roofing-offer-section .terms-wrapper .custom-accordion-item{background:#fff;border-radius:14px;overflow:hidden;border:none;}
|
||||
.roofing-offer-section summary.accordion-button{list-style:none;cursor:pointer;background:#fff;padding:16px 20px;font-size:15px;font-weight:700;color:#111;display:flex;justify-content:space-between;align-items:center;}
|
||||
.roofing-offer-section summary.accordion-button::-webkit-details-marker{display:none;}
|
||||
.roofing-offer-section .accordion-body{background:#fff;padding:8px 20px 20px;}
|
||||
.roofing-offer-section .accordion-body ul{margin:0;padding-left:20px;}
|
||||
.roofing-offer-section .accordion-body li{margin-bottom:10px;color:#444;font-size:13.5px;}
|
||||
.roofing-offer-section .deal-terms-list{list-style:none;padding-left:0;margin:0;}
|
||||
.roofing-offer-section .deal-terms-list li{margin-bottom:10px;color:#444;font-size:13.5px;line-height:1.5;}
|
||||
.roofing-offer-section .roofing-cta-box{padding:44px 32px;border-radius:22px;background:linear-gradient(90deg,rgba(255,140,0,0.25),rgba(20,150,255,0.22));backdrop-filter:blur(10px);}
|
||||
.roofing-offer-section .roofing-cta-box h2{color:#fff;font-size:32px;font-weight:800;line-height:1.2;margin-bottom:12px;}
|
||||
.roofing-offer-section .roofing-cta-box p{color:#d7d7d7;max-width:620px;margin:auto;font-size:15px;line-height:1.55;}
|
||||
.roofing-offer-section .cta-btn-group{margin-top:26px;display:flex;justify-content:center;gap:14px;flex-wrap:wrap;}
|
||||
.roofing-offer-section .start-btn,.roofing-offer-section .sales-btn{padding:12px 24px;border-radius:12px;text-decoration:none;font-size:14px;font-weight:700;transition:0.3s;}
|
||||
.roofing-offer-section .start-btn{background:linear-gradient(90deg,#ff9321,#4dc5ff);color:#fff;}
|
||||
.roofing-offer-section .sales-btn{background:rgba(255,255,255,0.08);color:#fff;border:1px solid rgba(255,255,255,0.1);}
|
||||
.roofing-offer-section .start-btn:hover,.roofing-offer-section .sales-btn:hover{transform:translateY(-3px);}
|
||||
@media (max-width:991px){
|
||||
.roofing-offer-section .col-lg-6{flex:0 0 100%;max-width:100%;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:32px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:28px;}
|
||||
.roofing-offer-section .price-box h2{font-size:44px;}
|
||||
}
|
||||
@media (max-width:767px){
|
||||
.roofing-offer-section{padding:48px 0;}
|
||||
.roofing-offer-section .roofing-heading h2{font-size:27px;}
|
||||
.roofing-offer-section .roofing-info-card h3{font-size:23px;}
|
||||
.roofing-offer-section .roofing-cta-box h2{font-size:24px;}
|
||||
.roofing-offer-section .lifetime-card,
|
||||
.roofing-offer-section .roofing-info-card,
|
||||
.roofing-offer-section .roofing-cta-box{padding:22px;}
|
||||
.roofing-offer-section .price-box h2{font-size:40px;}
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { Analytics, FieldIntelligence, Testimonials, Pricing, FinalCTA, Footer });
|
||||
@@ -0,0 +1,642 @@
|
||||
// Multi-step checkout wizard for the "Claim Founders Lifetime Deal" button
|
||||
// (.deal-btn). Opens as a modal and walks the visitor through:
|
||||
//
|
||||
// Step 1 Registration - contact + company + email + phone + sales reps +
|
||||
// number of licenses (1-10) + business address (with
|
||||
// API autofill) + SMS consent. Submitting captures the
|
||||
// record server-side (POST /api/register) BEFORE any
|
||||
// branch, so no lead is ever lost.
|
||||
// Branch on license count:
|
||||
// <= 5 Step 2 Cart + Terms - order summary (licenses x $2,000), the Deal
|
||||
// Terms & Conditions, and an "I agree" checkbox. Then
|
||||
// "Continue to Secure Payment" -> POST /api/checkout ->
|
||||
// redirect to Stripe. After payment Stripe returns the
|
||||
// buyer to /thanks.
|
||||
// > 5 Sales screen - "you may be eligible for a special discount" +
|
||||
// "Talk to Sales". No online payment (bulk orders are
|
||||
// handled by the sales team).
|
||||
//
|
||||
// WHY A CUSTOM FORM AND NOT STRIPE'S PAGE:
|
||||
// Customers could complete an Apple Pay purchase without giving us any contact
|
||||
// details, so there was no way to onboard buyers or distribute licenses. Stripe
|
||||
// hosted Checkout can't fix this (no way to hide the Apple Pay express button;
|
||||
// custom_fields enforcement there is undocumented) and SMS consent legally needs
|
||||
// our own disclaimer text. Collecting first makes the payment method irrelevant.
|
||||
// The server re-validates everything (api/_lead.js) — this form is UX only.
|
||||
//
|
||||
// Event delegation is used so this works on the static pages and the React home
|
||||
// page (where the button mounts after this script loads).
|
||||
(function () {
|
||||
// ---------------------------------------------------------------------------
|
||||
// TODO(client): replace with the exact SMS consent disclaimer Justin provided.
|
||||
// PLACEHOLDER wording, no legal pass. When it changes, bump SMS_CONSENT_VERSION
|
||||
// in api/checkout.js AND api/register.js so the CRM records the agreed version.
|
||||
// ---------------------------------------------------------------------------
|
||||
var SMS_CONSENT_TEXT =
|
||||
"I agree to receive SMS messages from Lynked Up Technologies about my Founders " +
|
||||
"License, onboarding, and product updates at the phone number provided. Message " +
|
||||
"frequency varies. Message and data rates may apply. Reply STOP to opt out or HELP " +
|
||||
"for help.";
|
||||
|
||||
// TODO(client): confirm the sales inbox for >5-license (bulk) enquiries.
|
||||
var SALES_EMAIL = "sales@lynkeduppro.com";
|
||||
|
||||
var SALES_REP_BUCKETS = ["1", "2-5", "6-10", "11-25", "26-50", "51+"];
|
||||
|
||||
// DISPLAY ONLY — the server owns the real charge (api/checkout.js). Keep in sync.
|
||||
var UNIT_PRICE = 2000; // USD per license
|
||||
var MAX_LICENSES = 10; // dropdown ceiling; must match api/_lead.js MAX_LICENSES
|
||||
var SELF_SERVE_MAX = 5; // <= this pays online; above -> sales. Match api/_lead.js
|
||||
|
||||
// Deal Terms & Conditions shown on the cart step. Mirrors the accordion text on
|
||||
// the pricing sections. TODO(client): keep in sync with Justin's final legal T&C.
|
||||
var TERMS = [
|
||||
"All claims, results, performance statements, platform capabilities, and customer references are provided for informational and promotional purposes only. Individual results may vary based on company size, user adoption, data quality, market conditions, workflow setup, and other business-specific factors.",
|
||||
"The Lifetime Founders License applies to one user license and one individual user account unless otherwise stated in writing. It includes access to core CRM features and the platform features made available under the applicable license terms. It does not guarantee access to every future product, premium module, third-party integration, usage-based service, or separately priced feature.",
|
||||
"SMS usage, data usage, storage, GPU processing, AI compute, automation volume, messaging volume, third-party API costs, and other usage-based services may incur additional monthly charges billed separately based on actual usage.",
|
||||
"Advanced or specialized features, including but not limited to Campaign-X, LiDAR scans, digital twin technology, AI-generated content, 3D property scans, mapping tools, and premium automation, may require additional fees, setup costs, subscriptions, or usage-based billing.",
|
||||
"Features, pricing, availability, and platform capabilities are subject to change. No statement shall be interpreted as a guarantee of revenue, cost savings, insurance savings, operational results, business growth, or specific customer outcomes. Use of the platform is subject to the final written agreement, subscription terms, billing terms, and applicable service terms provided by Lynked Up Technologies.",
|
||||
];
|
||||
|
||||
var FIELDS = [
|
||||
{ name: "firstName", label: "First name", type: "text", autocomplete: "given-name", placeholder: "Jane" },
|
||||
{ name: "lastName", label: "Last name", type: "text", autocomplete: "family-name", placeholder: "Smith" },
|
||||
{ name: "companyName", label: "Company name", type: "text", autocomplete: "organization", placeholder: "Smith Roofing LLC" },
|
||||
{ name: "email", label: "Email address", type: "email", autocomplete: "email", placeholder: "jane@smithroofing.com" },
|
||||
{ name: "phone", label: "Phone number", type: "tel", autocomplete: "tel", placeholder: "(555) 010-9999" },
|
||||
{ name: "salesReps", label: "Number of sales reps", type: "select", autocomplete: "off", options: SALES_REP_BUCKETS, empty: "Select…" },
|
||||
{ name: "licenses", label: "Number of licenses", type: "select", autocomplete: "off", options: licenseOptions(), value: "1" },
|
||||
// Street is full-width with autocomplete; selecting a suggestion fills the
|
||||
// city / state / zip fields below (which sit in their own 3-column row).
|
||||
{ name: "streetAddress", label: "Street address", type: "text", autocomplete: "off", full: true, placeholder: "Start typing your address…" },
|
||||
{ name: "city", label: "City", type: "text", autocomplete: "off", col3: true, placeholder: "Plano" },
|
||||
{ name: "state", label: "State", type: "text", autocomplete: "off", col3: true, placeholder: "TX" },
|
||||
{ name: "zip", label: "ZIP code", type: "text", autocomplete: "off", col3: true, placeholder: "75074" },
|
||||
];
|
||||
|
||||
function licenseOptions() {
|
||||
var a = [];
|
||||
for (var i = 1; i <= 10; i++) a.push(String(i));
|
||||
return a;
|
||||
}
|
||||
|
||||
var modal = null; // overlay element (built once)
|
||||
var body = null; // the step container inside the modal
|
||||
var activeBtn = null; // the .deal-btn that opened the modal
|
||||
var lastFocused = null; // element to restore focus to on close
|
||||
// plan: null = normal paid ($2,000/license). "demo" = fixed $1 Stripe test.
|
||||
// "free" = $0 no-card demo (page/19.html): runs the whole flow but skips Stripe
|
||||
// entirely and lands on /thanks, so the team can demo it without a card.
|
||||
var state = { step: "form", values: null, plan: null };
|
||||
|
||||
function esc(s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||||
});
|
||||
}
|
||||
function money(n) {
|
||||
return "$" + Number(n).toLocaleString("en-US");
|
||||
}
|
||||
function digitCount(v) {
|
||||
return (String(v).match(/\d/g) || []).length;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation. Mirrors api/_lead.js (server is authoritative; this is for UX).
|
||||
// ---------------------------------------------------------------------------
|
||||
function validate(values) {
|
||||
var errors = {};
|
||||
if (values.firstName.length < 1) errors.firstName = "Please enter your first name.";
|
||||
if (values.lastName.length < 1) errors.lastName = "Please enter your last name.";
|
||||
if (values.companyName.length < 2) errors.companyName = "Please enter your company name.";
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(values.email)) errors.email = "Please enter a valid email address.";
|
||||
var d = digitCount(values.phone);
|
||||
if (d < 10 || d > 15) errors.phone = "Please enter a valid phone number.";
|
||||
if (SALES_REP_BUCKETS.indexOf(values.salesReps) === -1) errors.salesReps = "Please select how many sales reps you have.";
|
||||
var lic = Number(values.licenses);
|
||||
if (!Number.isInteger(lic) || lic < 1 || lic > MAX_LICENSES) errors.licenses = "Please choose 1 to " + MAX_LICENSES + " licenses.";
|
||||
if (values.streetAddress.length < 3) errors.streetAddress = "Please enter your street address.";
|
||||
if (values.city.length < 2) errors.city = "Please enter your city.";
|
||||
if (values.state.length < 2) errors.state = "Please enter your state.";
|
||||
if (!/^\d{5}(-\d{4})?$/.test(values.zip)) errors.zip = "Please enter a valid ZIP code.";
|
||||
if (!values.smsConsent) errors.smsConsent = "Please agree to receive SMS updates to continue.";
|
||||
return errors;
|
||||
}
|
||||
|
||||
function readFormValues() {
|
||||
function val(n) {
|
||||
var el = body.querySelector('[name="' + n + '"]');
|
||||
return el ? String(el.value || "").trim() : "";
|
||||
}
|
||||
return {
|
||||
firstName: val("firstName"),
|
||||
lastName: val("lastName"),
|
||||
companyName: val("companyName"),
|
||||
email: val("email").toLowerCase(),
|
||||
phone: val("phone"),
|
||||
salesReps: val("salesReps"),
|
||||
licenses: parseInt(val("licenses"), 10) || 1,
|
||||
streetAddress: val("streetAddress"),
|
||||
city: val("city"),
|
||||
state: val("state"),
|
||||
zip: val("zip"),
|
||||
smsConsent: body.querySelector('[name="smsConsent"]').checked,
|
||||
};
|
||||
}
|
||||
|
||||
function showErrors(errors) {
|
||||
var names = FIELDS.map(function (f) { return f.name; }).concat(["smsConsent"]);
|
||||
var first = null;
|
||||
names.forEach(function (name) {
|
||||
var msgEl = body.querySelector('[data-err="' + name + '"]');
|
||||
var input = body.querySelector('[name="' + name + '"]');
|
||||
var msg = errors[name];
|
||||
if (msgEl) msgEl.textContent = msg || "";
|
||||
if (input) {
|
||||
input.setAttribute("aria-invalid", msg ? "true" : "false");
|
||||
if (msg && !first) first = input;
|
||||
}
|
||||
});
|
||||
if (first && first.focus) first.focus();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Address autocomplete. Provider: Photon (photon.komoot.io) — OpenStreetMap,
|
||||
// FREE, no API key, CORS-enabled. Chosen because no provider key was supplied.
|
||||
// TO SWAP FOR GOOGLE PLACES: replace geocode()'s body so it returns [{label}].
|
||||
// ---------------------------------------------------------------------------
|
||||
function geocode(query) {
|
||||
var url = "https://photon.komoot.io/api/?limit=5&lang=en&q=" + encodeURIComponent(query);
|
||||
return fetch(url)
|
||||
.then(function (r) { return r.ok ? r.json() : { features: [] }; })
|
||||
.then(function (data) {
|
||||
return (data.features || []).map(function (f) {
|
||||
var p = f.properties || {};
|
||||
var street = [p.housenumber, p.street].filter(Boolean).join(" ") ||
|
||||
(p.name && p.name !== (p.city || p.town) ? p.name : "");
|
||||
var city = p.city || p.town || p.village || p.county || "";
|
||||
var state = p.state || "";
|
||||
var zip = p.postcode || "";
|
||||
// Structured components used to fill the separate fields on select.
|
||||
var comp = { street: street, city: city, state: state, zip: zip };
|
||||
var label = [street, city, [state, zip].filter(Boolean).join(" "), p.country]
|
||||
.filter(Boolean).join(", ");
|
||||
comp.label = label;
|
||||
return comp;
|
||||
}).filter(function (s) { return s.label; });
|
||||
})
|
||||
.catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Fill the street field plus city/state/zip from a chosen suggestion, and
|
||||
// clear their error messages.
|
||||
function fillAddress(comp) {
|
||||
var map = { streetAddress: comp.street, city: comp.city, state: comp.state, zip: comp.zip };
|
||||
Object.keys(map).forEach(function (name) {
|
||||
var el = body.querySelector('[name="' + name + '"]');
|
||||
if (el && map[name]) {
|
||||
el.value = map[name];
|
||||
el.setAttribute("aria-invalid", "false");
|
||||
}
|
||||
var msg = body.querySelector('[data-err="' + name + '"]');
|
||||
if (msg && map[name]) msg.textContent = "";
|
||||
});
|
||||
}
|
||||
|
||||
function attachAddressAutocomplete(input) {
|
||||
var box = document.createElement("ul");
|
||||
box.className = "lu-ac";
|
||||
box.setAttribute("hidden", "");
|
||||
input.parentNode.appendChild(box);
|
||||
var timer = null, lastQuery = "";
|
||||
|
||||
function hide() { box.setAttribute("hidden", ""); box.innerHTML = ""; }
|
||||
function render(items) {
|
||||
box.innerHTML = "";
|
||||
if (!items.length) { hide(); return; }
|
||||
items.forEach(function (it) {
|
||||
var li = document.createElement("li");
|
||||
li.className = "lu-ac-item";
|
||||
li.textContent = it.label;
|
||||
li.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
// Prefer the parsed street; fall back to the full label if none.
|
||||
input.value = it.street || it.label;
|
||||
fillAddress(it);
|
||||
hide();
|
||||
});
|
||||
box.appendChild(li);
|
||||
});
|
||||
box.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
input.addEventListener("input", function () {
|
||||
var q = input.value.trim();
|
||||
if (timer) clearTimeout(timer);
|
||||
if (q.length < 4) { hide(); return; }
|
||||
timer = setTimeout(function () {
|
||||
if (q === lastQuery) return;
|
||||
lastQuery = q;
|
||||
geocode(q).then(function (items) { if (input.value.trim() === q) render(items); });
|
||||
}, 250);
|
||||
});
|
||||
input.addEventListener("blur", function () { setTimeout(hide, 120); });
|
||||
input.addEventListener("keydown", function (e) { if (e.key === "Escape") hide(); });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Markup helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
function fieldHtml(f) {
|
||||
var id = "lu-f-" + f.name;
|
||||
var common = 'id="' + id + '" name="' + f.name + '" autocomplete="' + f.autocomplete +
|
||||
'" aria-describedby="lu-e-' + f.name + '" class="lu-input"';
|
||||
var control;
|
||||
if (f.type === "select") {
|
||||
var opts = (f.empty ? '<option value="">' + esc(f.empty) + "</option>" : "") +
|
||||
f.options.map(function (o) {
|
||||
var sel = f.value === o ? " selected" : "";
|
||||
return '<option value="' + esc(o) + '"' + sel + ">" + esc(o) + "</option>";
|
||||
}).join("");
|
||||
control = "<select " + common + ">" + opts + "</select>";
|
||||
} else {
|
||||
control = "<input " + common + ' type="' + f.type + '" placeholder="' + esc(f.placeholder || "") + '" />';
|
||||
}
|
||||
return (
|
||||
'<div class="lu-field' + (f.full ? " lu-field-full" : "") + '">' +
|
||||
'<label class="lu-label" for="' + id + '">' + esc(f.label) + ' <span class="lu-req">*</span></label>' +
|
||||
control +
|
||||
'<div class="lu-err" id="lu-e-' + f.name + '" data-err="' + f.name + '" aria-live="polite"></div>' +
|
||||
"</div>"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 1 — registration form
|
||||
// ---------------------------------------------------------------------------
|
||||
function renderForm() {
|
||||
state.step = "form";
|
||||
body.innerHTML =
|
||||
'<h2 class="lu-title">Claim your Founders License</h2>' +
|
||||
'<p class="lu-sub">Tell us a bit about your business so we can set up your account and licenses.</p>' +
|
||||
'<form class="lu-form" novalidate>' +
|
||||
'<div class="lu-grid">' + FIELDS.filter(function (f) { return !f.col3; }).map(fieldHtml).join("") + "</div>" +
|
||||
'<div class="lu-grid3">' + FIELDS.filter(function (f) { return f.col3; }).map(fieldHtml).join("") + "</div>" +
|
||||
'<div class="lu-subtotal" data-subtotal></div>' +
|
||||
'<div class="lu-consent">' +
|
||||
'<label class="lu-check"><input type="checkbox" name="smsConsent" aria-describedby="lu-e-smsConsent" />' +
|
||||
'<span class="lu-consent-text">' + esc(SMS_CONSENT_TEXT) + "</span></label>" +
|
||||
'<div class="lu-err" data-err="smsConsent" id="lu-e-smsConsent" aria-live="polite"></div>' +
|
||||
"</div>" +
|
||||
'<div class="lu-form-err" data-form-err aria-live="polite"></div>' +
|
||||
'<button type="submit" class="lu-submit">Continue</button>' +
|
||||
'<p class="lu-fine">All fields are required. Next you\'ll review your order.</p>' +
|
||||
"</form>";
|
||||
|
||||
// Restore previously entered values if the buyer stepped back.
|
||||
if (state.values) {
|
||||
Object.keys(state.values).forEach(function (k) {
|
||||
var el = body.querySelector('[name="' + k + '"]');
|
||||
if (!el) return;
|
||||
if (el.type === "checkbox") el.checked = !!state.values[k];
|
||||
else el.value = state.values[k];
|
||||
});
|
||||
}
|
||||
|
||||
body.querySelector("form").addEventListener("submit", onFormSubmit);
|
||||
var addr = body.querySelector('[name="streetAddress"]');
|
||||
if (addr) attachAddressAutocomplete(addr);
|
||||
var lic = body.querySelector('[name="licenses"]');
|
||||
if (lic) { lic.addEventListener("change", updateSubtotal); }
|
||||
updateSubtotal();
|
||||
focusFirst('[name="firstName"]');
|
||||
}
|
||||
|
||||
function updateSubtotal() {
|
||||
var el = body.querySelector("[data-subtotal]");
|
||||
var lic = body.querySelector('[name="licenses"]');
|
||||
if (!el || !lic) return;
|
||||
var n = parseInt(lic.value, 10) || 1;
|
||||
if (n > SELF_SERVE_MAX) {
|
||||
el.innerHTML = '<span class="lu-subtotal-note">' + n + " licenses — you may qualify for special founder pricing. We'll take your details next.</span>";
|
||||
} else {
|
||||
el.innerHTML = "<span>" + n + " × " + money(UNIT_PRICE) + "</span><strong>" + money(UNIT_PRICE * n) + "</strong>";
|
||||
}
|
||||
}
|
||||
|
||||
function onFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
var values = readFormValues();
|
||||
var errors = validate(values);
|
||||
body.querySelector("[data-form-err]").textContent = "";
|
||||
showErrors(errors);
|
||||
if (Object.keys(errors).length) return;
|
||||
state.values = values;
|
||||
|
||||
setSubmitting(true, "Saving your details…");
|
||||
// Capture the record server-side FIRST (both branches), then route.
|
||||
fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(values),
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); })
|
||||
.then(function (res) {
|
||||
setSubmitting(false);
|
||||
if (!res.ok) {
|
||||
if (res.data && res.data.fields) { showErrors(res.data.fields); return; }
|
||||
throw new Error((res.data && res.data.error) || "Could not save your details.");
|
||||
}
|
||||
var path = (res.data && res.data.path) || (values.licenses > SELF_SERVE_MAX ? "sales" : "checkout");
|
||||
if (path === "sales") renderSales();
|
||||
else renderCart();
|
||||
})
|
||||
.catch(function (err) {
|
||||
setSubmitting(false);
|
||||
console.error("Register error:", err);
|
||||
var fe = body.querySelector("[data-form-err]");
|
||||
if (fe) fe.textContent = "Sorry, something went wrong. Please try again in a moment.";
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 2 — cart + terms (<= SELF_SERVE_MAX licenses)
|
||||
// ---------------------------------------------------------------------------
|
||||
function renderCart() {
|
||||
state.step = "cart";
|
||||
var n = state.values.licenses;
|
||||
var free = state.plan === "free";
|
||||
var unit = free ? 0 : UNIT_PRICE;
|
||||
var total = unit * n;
|
||||
body.innerHTML =
|
||||
'<button type="button" class="lu-back" data-back>← Back</button>' +
|
||||
'<h2 class="lu-title">' + (free ? "Review your demo" : "Review your order") + "</h2>" +
|
||||
'<p class="lu-sub">' +
|
||||
(free
|
||||
? "Demo access — no payment and no card required."
|
||||
: "Founders Lifetime License — one-time payment, use forever.") +
|
||||
"</p>" +
|
||||
(free ? '<div class="lu-demo-flag">🎬 Demo mode · $0 · no card</div>' : "") +
|
||||
'<div class="lu-cart">' +
|
||||
'<div class="lu-cart-row"><span>Founders Lifetime License' + (free ? " (demo)" : "") + "</span><span>" + money(unit) + " × " + n + "</span></div>" +
|
||||
'<div class="lu-cart-line"></div>' +
|
||||
'<div class="lu-cart-row lu-cart-total"><span>Total today</span><span>' + money(total) + "</span></div>" +
|
||||
'<div class="lu-cart-sub">' + (free ? "Demo · no charge" : "One-time payment") + " · " + n + (n > 1 ? " licenses" : " license") + "</div>" +
|
||||
"</div>" +
|
||||
'<div class="lu-terms-label">Deal Terms & Conditions</div>' +
|
||||
'<div class="lu-terms">' + TERMS.map(function (p) { return "<p>" + esc(p) + "</p>"; }).join("") + "</div>" +
|
||||
'<div class="lu-consent">' +
|
||||
'<label class="lu-check"><input type="checkbox" name="termsAgree" />' +
|
||||
'<span class="lu-consent-text">I have read and agree to the Deal Terms & Conditions above.</span></label>' +
|
||||
'<div class="lu-err" data-err="termsAgree" aria-live="polite"></div>' +
|
||||
"</div>" +
|
||||
'<div class="lu-form-err" data-form-err aria-live="polite"></div>' +
|
||||
'<button type="button" class="lu-submit" data-pay>' +
|
||||
(free ? "Complete demo" : "Continue to Secure Payment") + "</button>" +
|
||||
'<p class="lu-fine">' +
|
||||
(free
|
||||
? "No card needed — this is a demo of the full flow."
|
||||
: "You'll be redirected to Stripe to complete payment securely.") +
|
||||
"</p>";
|
||||
|
||||
body.querySelector("[data-back]").addEventListener("click", renderForm);
|
||||
body.querySelector("[data-pay]").addEventListener("click", onPay);
|
||||
focusFirst("[data-pay]");
|
||||
}
|
||||
|
||||
function onPay() {
|
||||
var agree = body.querySelector('[name="termsAgree"]');
|
||||
var errEl = body.querySelector('[data-err="termsAgree"]');
|
||||
if (!agree.checked) {
|
||||
if (errEl) errEl.textContent = "Please agree to the terms to continue.";
|
||||
agree.focus();
|
||||
return;
|
||||
}
|
||||
if (errEl) errEl.textContent = "";
|
||||
|
||||
// $0 no-card demo (page/19.html): the record was already captured at Step 1,
|
||||
// so just land on the Thanks page — no Stripe, no card. Full flow, zero cost.
|
||||
if (state.plan === "free") {
|
||||
setSubmitting(true, "Finishing demo…", "[data-pay]");
|
||||
window.location.href = "/thanks?demo=1";
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true, "Loading checkout…", "[data-pay]");
|
||||
|
||||
var plan = activeBtn ? activeBtn.getAttribute("data-plan") : null;
|
||||
var promoEl = document.querySelector("[data-promo-input]");
|
||||
var promo = promoEl && promoEl.value ? promoEl.value.trim() : "";
|
||||
var qs = [];
|
||||
if (plan) qs.push("plan=" + encodeURIComponent(plan));
|
||||
if (promo) qs.push("promo=" + encodeURIComponent(promo));
|
||||
var endpoint = "/api/checkout" + (qs.length ? "?" + qs.join("&") : "");
|
||||
|
||||
var payload = Object.assign({}, state.values, { termsAgreed: true });
|
||||
fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, data: d }; }); })
|
||||
.then(function (res) {
|
||||
if (res.ok && res.data && res.data.url) { window.location.href = res.data.url; return; }
|
||||
// Server guard: bulk orders route to sales even if we reach here.
|
||||
if (res.data && res.data.path === "sales") { renderSales(); return; }
|
||||
setSubmitting(false, null, "[data-pay]");
|
||||
var fe = body.querySelector("[data-form-err]");
|
||||
if (fe) fe.textContent = (res.data && res.data.error) || "We couldn't start checkout. Please try again.";
|
||||
})
|
||||
.catch(function (err) {
|
||||
setSubmitting(false, null, "[data-pay]");
|
||||
console.error("Checkout error:", err);
|
||||
var fe = body.querySelector("[data-form-err]");
|
||||
if (fe) fe.textContent = "Sorry, we couldn't start the checkout. Please try again in a moment.";
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sales screen (> SELF_SERVE_MAX licenses)
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildMailto() {
|
||||
var v = state.values || {};
|
||||
var name = [v.firstName, v.lastName].filter(Boolean).join(" ");
|
||||
var address = [v.streetAddress, v.city, [v.state, v.zip].filter(Boolean).join(" ")]
|
||||
.filter(Boolean).join(", ");
|
||||
var subject = "Founders License — bulk order (" + (v.licenses || "") + " licenses)";
|
||||
var lines = [
|
||||
"Hi LynkedUp Pro team,",
|
||||
"",
|
||||
"I'd like to discuss a bulk Founders License order.",
|
||||
"",
|
||||
"Name: " + name,
|
||||
"Company: " + (v.companyName || ""),
|
||||
"Email: " + (v.email || ""),
|
||||
"Phone: " + (v.phone || ""),
|
||||
"Sales reps: " + (v.salesReps || ""),
|
||||
"Licenses wanted: " + (v.licenses || ""),
|
||||
"Business address: " + address,
|
||||
];
|
||||
return "mailto:" + SALES_EMAIL + "?subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(lines.join("\n"));
|
||||
}
|
||||
|
||||
function renderSales() {
|
||||
state.step = "sales";
|
||||
var n = state.values.licenses;
|
||||
body.innerHTML =
|
||||
'<button type="button" class="lu-back" data-back>← Back</button>' +
|
||||
'<div class="lu-celebrate">🎉</div>' +
|
||||
'<h2 class="lu-title lu-center">Congratulations — you may be eligible for a special discount</h2>' +
|
||||
'<p class="lu-sub lu-center">You\'re looking at <strong>' + n + " licenses</strong>. Orders above " + SELF_SERVE_MAX +
|
||||
" licenses qualify for special founder pricing that isn't available online. Your details are saved — our team will reach out, or you can start the conversation now." +
|
||||
"</p>" +
|
||||
'<a class="lu-submit lu-link-btn" href="' + buildMailto() + '">Talk to Sales</a>' +
|
||||
'<p class="lu-fine">Prefer fewer licenses? Go back and choose ' + SELF_SERVE_MAX + " or fewer to check out instantly.</p>";
|
||||
body.querySelector("[data-back]").addEventListener("click", renderForm);
|
||||
focusFirst(".lu-link-btn");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared modal chrome
|
||||
// ---------------------------------------------------------------------------
|
||||
function focusFirst(sel) {
|
||||
var el = body.querySelector(sel);
|
||||
if (el && el.focus) el.focus();
|
||||
}
|
||||
|
||||
function setSubmitting(on, label, sel) {
|
||||
var b = body.querySelector(sel || ".lu-submit");
|
||||
if (!b) return;
|
||||
b.disabled = !!on;
|
||||
if (on) { b.dataset.orig = b.dataset.orig || b.textContent; b.textContent = label || "Loading…"; }
|
||||
else if (b.dataset.orig) b.textContent = b.dataset.orig;
|
||||
}
|
||||
|
||||
var CSS =
|
||||
".lu-overlay{position:fixed;inset:0;z-index:2147483000;background:rgba(6,6,10,.78);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding:24px 16px;overflow-y:auto;}" +
|
||||
".lu-overlay[hidden]{display:none;}" +
|
||||
".lu-modal{position:relative;width:100%;max-width:560px;margin:auto;background:#121216;border:1px solid rgba(255,255,255,.10);border-radius:18px;padding:28px;box-shadow:0 30px 80px rgba(0,0,0,.6);font-family:inherit;}" +
|
||||
".lu-close{position:absolute;top:14px;right:16px;width:32px;height:32px;border:0;border-radius:8px;background:rgba(255,255,255,.06);color:#fff;font-size:20px;line-height:1;cursor:pointer;z-index:2;}" +
|
||||
".lu-close:hover{background:rgba(255,255,255,.12);}" +
|
||||
".lu-back{background:none;border:0;color:#9a9aa2;font-size:13px;font-family:inherit;cursor:pointer;padding:0;margin:0 0 12px;}" +
|
||||
".lu-back:hover{color:#fff;}" +
|
||||
".lu-title{margin:0 0 6px;color:#fff;font-size:22px;font-weight:800;letter-spacing:-.01em;line-height:1.25;}" +
|
||||
".lu-center{text-align:center;}" +
|
||||
".lu-sub{margin:0 0 20px;color:#9a9aa2;font-size:13.5px;line-height:1.55;}" +
|
||||
".lu-sub strong{color:#fff;}" +
|
||||
".lu-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}" +
|
||||
".lu-field-full{grid-column:1/-1;position:relative;}" +
|
||||
".lu-grid3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:14px;margin-top:14px;}" +
|
||||
"@media(max-width:560px){.lu-grid3{grid-template-columns:1fr 1fr;}}" +
|
||||
".lu-label{display:block;margin-bottom:6px;color:#e6e6ea;font-size:12.5px;font-weight:600;}" +
|
||||
".lu-req{color:#ff9321;}" +
|
||||
".lu-input{width:100%;box-sizing:border-box;padding:11px 13px;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:#fff;font-size:14px;font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s;}" +
|
||||
".lu-input::placeholder{color:#6f6f78;}" +
|
||||
".lu-input:focus{border-color:#4dc5ff;box-shadow:0 0 0 3px rgba(77,197,255,.15);}" +
|
||||
'.lu-input[aria-invalid="true"]{border-color:#ff5f56;}' +
|
||||
"select.lu-input{appearance:none;cursor:pointer;}select.lu-input option{background:#121216;color:#fff;}" +
|
||||
".lu-ac{position:absolute;left:0;right:0;top:100%;margin:4px 0 0;padding:4px;list-style:none;z-index:5;background:#1b1b21;border:1px solid rgba(255,255,255,.14);border-radius:10px;box-shadow:0 18px 40px rgba(0,0,0,.5);max-height:220px;overflow-y:auto;}" +
|
||||
".lu-ac[hidden]{display:none;}" +
|
||||
".lu-ac-item{padding:9px 11px;border-radius:7px;color:#e6e6ea;font-size:13px;cursor:pointer;}" +
|
||||
".lu-ac-item:hover{background:rgba(77,197,255,.14);}" +
|
||||
".lu-subtotal{display:flex;align-items:center;justify-content:space-between;margin:14px 0 2px;padding:10px 14px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);color:#c9c9d2;font-size:13px;}" +
|
||||
".lu-subtotal strong{color:#fff;font-size:16px;}" +
|
||||
".lu-subtotal-note{color:#ffb066;font-size:12.5px;line-height:1.4;}" +
|
||||
".lu-cart{margin:0 0 18px;padding:18px;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.09);}" +
|
||||
".lu-cart-row{display:flex;align-items:center;justify-content:space-between;color:#e6e6ea;font-size:14px;}" +
|
||||
".lu-cart-line{height:1px;background:rgba(255,255,255,.10);margin:14px 0;}" +
|
||||
".lu-cart-total{font-weight:800;}" +
|
||||
".lu-cart-total span:last-child{font-size:24px;color:#fff;}" +
|
||||
".lu-cart-sub{color:#8a8a93;font-size:11.5px;margin-top:6px;}" +
|
||||
".lu-terms-label{color:#e6e6ea;font-size:12.5px;font-weight:700;margin:0 0 6px;}" +
|
||||
".lu-terms{max-height:150px;overflow-y:auto;padding:12px 14px;border-radius:10px;background:rgba(0,0,0,.25);border:1px solid rgba(255,255,255,.08);color:#9a9aa2;font-size:11.5px;line-height:1.6;}" +
|
||||
".lu-terms p{margin:0 0 9px;}.lu-terms p:last-child{margin:0;}" +
|
||||
".lu-consent{margin:16px 0 4px;}" +
|
||||
".lu-check{display:flex;gap:10px;align-items:flex-start;cursor:pointer;}" +
|
||||
".lu-check input{margin-top:2px;width:16px;height:16px;flex:0 0 auto;accent-color:#4dc5ff;cursor:pointer;}" +
|
||||
".lu-consent-text{color:#9a9aa2;font-size:11.5px;line-height:1.5;}" +
|
||||
".lu-form-err{color:#ff6b61;font-size:12.5px;min-height:16px;margin:6px 0;}" +
|
||||
".lu-err{color:#ff6b61;font-size:11.5px;margin-top:4px;min-height:14px;}" +
|
||||
".lu-submit{display:block;width:100%;margin-top:8px;padding:14px 24px;border:0;border-radius:12px;color:#fff;font-size:15px;font-weight:700;font-family:inherit;cursor:pointer;text-align:center;text-decoration:none;box-sizing:border-box;background:linear-gradient(90deg,#ff9321,#4dc5ff);box-shadow:0 10px 25px rgba(0,0,0,.3);}" +
|
||||
".lu-submit:disabled{opacity:.6;cursor:default;}" +
|
||||
".lu-link-btn{margin-top:14px;}" +
|
||||
".lu-demo-flag{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;background:rgba(77,197,255,.14);border:1px solid rgba(77,197,255,.35);color:#8fd6ff;font-size:11.5px;font-weight:700;}" +
|
||||
".lu-celebrate{font-size:44px;text-align:center;margin:4px 0 10px;}" +
|
||||
".lu-fine{margin:12px 0 0;text-align:center;color:#6f6f78;font-size:11px;}" +
|
||||
"@media(max-width:560px){.lu-modal{padding:22px 18px;}.lu-grid{grid-template-columns:1fr;}}";
|
||||
|
||||
function injectCss() {
|
||||
if (document.getElementById("lu-checkout-css")) return;
|
||||
var s = document.createElement("style");
|
||||
s.id = "lu-checkout-css";
|
||||
s.textContent = CSS;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function build() {
|
||||
var el = document.createElement("div");
|
||||
el.className = "lu-overlay";
|
||||
el.setAttribute("hidden", "");
|
||||
el.innerHTML =
|
||||
'<div class="lu-modal" role="dialog" aria-modal="true">' +
|
||||
'<button type="button" class="lu-close" aria-label="Close">×</button>' +
|
||||
'<div class="lu-body"></div>' +
|
||||
"</div>";
|
||||
el.addEventListener("click", function (e) { if (e.target === el) close(); });
|
||||
el.querySelector(".lu-close").addEventListener("click", close);
|
||||
document.body.appendChild(el);
|
||||
body = el.querySelector(".lu-body");
|
||||
return el;
|
||||
}
|
||||
|
||||
function open(btn) {
|
||||
injectCss();
|
||||
if (!modal) modal = build();
|
||||
activeBtn = btn;
|
||||
lastFocused = document.activeElement;
|
||||
// keep prior entries if reopened; read the plan off the clicked button
|
||||
state = { step: "form", values: state.values, plan: btn.getAttribute("data-plan") };
|
||||
modal.removeAttribute("hidden");
|
||||
document.body.style.overflow = "hidden";
|
||||
renderForm();
|
||||
document.addEventListener("keydown", onKeydown, true);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!modal || modal.hasAttribute("hidden")) return;
|
||||
modal.setAttribute("hidden", "");
|
||||
document.body.style.overflow = "";
|
||||
document.removeEventListener("keydown", onKeydown, true);
|
||||
if (lastFocused && lastFocused.focus) lastFocused.focus();
|
||||
activeBtn = null;
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === "Escape") { close(); return; }
|
||||
if (e.key !== "Tab") return;
|
||||
var f = modal.querySelectorAll("button, input, select, textarea, a[href]");
|
||||
var list = [];
|
||||
for (var i = 0; i < f.length; i++) if (!f[i].disabled && f[i].offsetParent !== null) list.push(f[i]);
|
||||
if (!list.length) return;
|
||||
var firstEl = list[0], lastEl = list[list.length - 1];
|
||||
if (e.shiftKey && document.activeElement === firstEl) { e.preventDefault(); lastEl.focus(); }
|
||||
else if (!e.shiftKey && document.activeElement === lastEl) { e.preventDefault(); firstEl.focus(); }
|
||||
}
|
||||
|
||||
// Re-enable a stuck button after a bfcache restore (e.g. back from Stripe).
|
||||
window.addEventListener("pageshow", function (e) {
|
||||
if (e.persisted && body) { var b = body.querySelector(".lu-submit"); if (b) { b.disabled = false; if (b.dataset.orig) b.textContent = b.dataset.orig; } }
|
||||
});
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
var target = e.target;
|
||||
var btn = target && target.closest ? target.closest(".deal-btn") : null;
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
open(btn);
|
||||
},
|
||||
true
|
||||
);
|
||||
})();
|
||||
@@ -0,0 +1,823 @@
|
||||
/* LynkedUp Pro — Cinematic Neon Design System */
|
||||
|
||||
:root {
|
||||
--bg: #03060c;
|
||||
--bg-2: #060c18;
|
||||
--bg-3: #0a1326;
|
||||
--ink: #e8f4ff;
|
||||
--ink-2: #b6c8df;
|
||||
--muted: rgba(232, 244, 255, 0.55);
|
||||
--dim: rgba(232, 244, 255, 0.35);
|
||||
|
||||
--cyan: #00d1ff;
|
||||
--cyan-2: #38e6ff;
|
||||
--cyan-deep: #0070a0;
|
||||
--orange: #ff8a1e;
|
||||
--orange-2: #ffb24d;
|
||||
--green: #2bd49c;
|
||||
--violet: #8a6bff;
|
||||
--magenta: #ff5cf0;
|
||||
|
||||
--glass: rgba(10, 22, 40, 0.55);
|
||||
--glass-2: rgba(10, 22, 40, 0.35);
|
||||
--border: rgba(0, 209, 255, 0.18);
|
||||
--border-strong: rgba(0, 209, 255, 0.42);
|
||||
|
||||
--r-sm: 10px;
|
||||
--r-md: 14px;
|
||||
--r-lg: 22px;
|
||||
--r-xl: 32px;
|
||||
|
||||
--shadow-cyan: 0 0 0 1px rgba(0, 209, 255, 0.25), 0 12px 60px -10px rgba(0, 209, 255, 0.45);
|
||||
--shadow-cyan-soft: 0 0 0 1px rgba(0, 209, 255, 0.12), 0 18px 80px -20px rgba(0, 209, 255, 0.35);
|
||||
|
||||
--font-display: "Space Grotesk", "Satoshi", "Inter", system-ui, sans-serif;
|
||||
--font-body: "Inter", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body);
|
||||
font-feature-settings: "ss01", "cv11";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Page-wide animated backdrop --------------------------------- */
|
||||
.app {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Floor + ceiling grid + ambient radial */
|
||||
.bg-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(900px 600px at 18% -10%, rgba(0, 130, 200, 0.28), transparent 60%),
|
||||
radial-gradient(1100px 700px at 95% 10%, rgba(120, 60, 255, 0.18), transparent 65%),
|
||||
radial-gradient(800px 500px at 50% 120%, rgba(0, 209, 255, 0.18), transparent 60%),
|
||||
linear-gradient(180deg, #03060c 0%, #04081a 50%, #03060c 100%);
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 209, 255, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 209, 255, 0.06) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
mask-image: radial-gradient(ellipse 100% 70% at 50% 30%, #000 30%, transparent 80%);
|
||||
animation: gridPan 60s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gridPan {
|
||||
to { background-position: 60px 60px; }
|
||||
}
|
||||
|
||||
/* Mouse-follow spotlight */
|
||||
.cursor-glow {
|
||||
position: fixed;
|
||||
z-index: 0;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle, rgba(0, 209, 255, 0.18), rgba(0, 209, 255, 0) 60%);
|
||||
transform: translate(-50%, -50%);
|
||||
mix-blend-mode: screen;
|
||||
transition: opacity 0.4s ease;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Floating particles */
|
||||
.particles {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 2px; height: 2px;
|
||||
background: var(--cyan);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--cyan), 0 0 18px rgba(0, 209, 255, 0.6);
|
||||
opacity: 0.5;
|
||||
animation: floatUp linear infinite;
|
||||
}
|
||||
@keyframes floatUp {
|
||||
0% { transform: translateY(0) translateX(0); opacity: 0; }
|
||||
10% { opacity: 0.7; }
|
||||
90% { opacity: 0.7; }
|
||||
100% { transform: translateY(-110vh) translateX(40px); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Typography ---------------------------------------------------- */
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-display);
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font-mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.22em;
|
||||
font-size: 11px;
|
||||
color: var(--cyan-2);
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 209, 255, 0.05);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.eyebrow .dot {
|
||||
width: 6px; height: 6px;
|
||||
background: var(--cyan);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--cyan);
|
||||
animation: pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.h-display {
|
||||
font-size: clamp(48px, 7vw, 96px);
|
||||
line-height: 0.96;
|
||||
letter-spacing: -0.035em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.h-display .accent { color: var(--cyan); }
|
||||
.h-display .grad {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #c1e8ff 60%, #5fc8ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.h-display .underline {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: var(--cyan);
|
||||
}
|
||||
.h-display .underline::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
bottom: 0.05em;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
|
||||
box-shadow: 0 0 18px var(--cyan);
|
||||
}
|
||||
|
||||
.h-section {
|
||||
font-size: clamp(38px, 5vw, 68px);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.h-section .accent { color: var(--cyan); }
|
||||
.h-card {
|
||||
font-size: 20px;
|
||||
line-height: 1.15;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sub {
|
||||
color: var(--ink-2);
|
||||
font-size: 17px;
|
||||
line-height: 1.55;
|
||||
max-width: 56ch;
|
||||
}
|
||||
|
||||
/* Layout -------------------------------------------------------- */
|
||||
.shell {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 48px;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.shell { padding: 0 22px; }
|
||||
}
|
||||
|
||||
section.section {
|
||||
position: relative;
|
||||
padding: 140px 0;
|
||||
}
|
||||
section.section.tight { padding: 90px 0; }
|
||||
|
||||
/* Section divider beam */
|
||||
.beam {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(1100px, 80%);
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 209, 255, 0.6), transparent);
|
||||
box-shadow: 0 0 30px rgba(0, 209, 255, 0.5);
|
||||
}
|
||||
.beam.top { top: 0; }
|
||||
.beam.bottom { bottom: 0; }
|
||||
|
||||
/* Glass card ----------------------------------------------------- */
|
||||
.card {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(12, 26, 48, 0.7), rgba(6, 14, 28, 0.55));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
overflow: hidden;
|
||||
transition: transform 0.4s cubic-bezier(.2,.7,.2,1), border-color 0.3s, box-shadow 0.4s;
|
||||
}
|
||||
.card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
400px circle at var(--mx, 50%) var(--my, 0%),
|
||||
rgba(0, 209, 255, 0.18),
|
||||
transparent 45%
|
||||
);
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card:hover::before { opacity: 1; }
|
||||
.card:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-cyan-soft);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.card .corner {
|
||||
position: absolute;
|
||||
width: 14px; height: 14px;
|
||||
border: 1px solid var(--cyan);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.card .corner.tl { top: 8px; left: 8px; border-right: 0; border-bottom: 0; }
|
||||
.card .corner.tr { top: 8px; right: 8px; border-left: 0; border-bottom: 0; }
|
||||
.card .corner.bl { bottom: 8px; left: 8px; border-right: 0; border-top: 0; }
|
||||
.card .corner.br { bottom: 8px; right: 8px; border-left: 0; border-top: 0; }
|
||||
|
||||
.card .pad { padding: 28px; }
|
||||
.card .pad-lg { padding: 36px; }
|
||||
.card .pad-sm { padding: 18px; }
|
||||
|
||||
/* Buttons ------------------------------------------------------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 52px;
|
||||
padding: 0 26px;
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.005em;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.3s, background 0.3s, border-color 0.3s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(180deg, #00d1ff 0%, #0094c8 100%);
|
||||
color: #001520;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 209, 255, 0.6) inset,
|
||||
0 12px 30px -10px rgba(0, 209, 255, 0.7),
|
||||
0 0 60px -10px rgba(0, 209, 255, 0.55);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 209, 255, 0.9) inset,
|
||||
0 18px 40px -10px rgba(0, 209, 255, 0.9),
|
||||
0 0 80px -8px rgba(0, 209, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-orange {
|
||||
background: linear-gradient(180deg, #ffb24d 0%, #ff7a00 100%);
|
||||
color: #1a0800;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 138, 30, 0.6) inset,
|
||||
0 12px 30px -10px rgba(255, 138, 30, 0.7),
|
||||
0 0 60px -10px rgba(255, 138, 30, 0.55);
|
||||
}
|
||||
.btn-orange:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 138, 30, 0.9) inset,
|
||||
0 18px 40px -10px rgba(255, 138, 30, 0.9),
|
||||
0 0 80px -8px rgba(255, 138, 30, 0.7);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: rgba(10, 22, 40, 0.5);
|
||||
border-color: var(--border);
|
||||
color: var(--ink);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
border-color: var(--border-strong);
|
||||
background: rgba(0, 209, 255, 0.08);
|
||||
box-shadow: 0 0 0 1px var(--border-strong), 0 0 30px -8px var(--cyan);
|
||||
}
|
||||
|
||||
.btn .arrow {
|
||||
display: inline-block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
.btn:hover .arrow { transform: translateX(4px); }
|
||||
|
||||
/* Pill / Chip --------------------------------------------------- */
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 209, 255, 0.06);
|
||||
color: var(--cyan-2);
|
||||
}
|
||||
.chip.orange {
|
||||
border-color: rgba(255, 138, 30, 0.35);
|
||||
background: rgba(255, 138, 30, 0.08);
|
||||
color: var(--orange-2);
|
||||
}
|
||||
.chip.green {
|
||||
border-color: rgba(43, 212, 156, 0.3);
|
||||
background: rgba(43, 212, 156, 0.08);
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* Reveal animation (scroll trigger) ----------------------------- */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition: opacity 0.9s cubic-bezier(.2,.7,.2,1), transform 0.9s cubic-bezier(.2,.7,.2,1);
|
||||
}
|
||||
.reveal.in {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.reveal.delay-1 { transition-delay: 0.08s; }
|
||||
.reveal.delay-2 { transition-delay: 0.16s; }
|
||||
.reveal.delay-3 { transition-delay: 0.24s; }
|
||||
.reveal.delay-4 { transition-delay: 0.32s; }
|
||||
.reveal.delay-5 { transition-delay: 0.4s; }
|
||||
|
||||
/* Helpers ------------------------------------------------------- */
|
||||
.row { display: flex; align-items: center; }
|
||||
.gap-8 { gap: 8px; }
|
||||
.gap-12 { gap: 12px; }
|
||||
.gap-16 { gap: 16px; }
|
||||
.gap-24 { gap: 24px; }
|
||||
.muted { color: var(--muted); }
|
||||
.dim { color: var(--dim); }
|
||||
.mono { font-family: var(--font-mono); }
|
||||
.tabular { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* Glow rings */
|
||||
.ring-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Animated counter dot */
|
||||
.live-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 10px var(--green);
|
||||
animation: pulse 1.4s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Wireframe line drawing */
|
||||
@keyframes drawIn {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
.wf-line {
|
||||
stroke: var(--cyan);
|
||||
fill: none;
|
||||
stroke-width: 1.5;
|
||||
filter: drop-shadow(0 0 6px rgba(0, 209, 255, 0.7));
|
||||
}
|
||||
|
||||
/* Marquee tape */
|
||||
.marquee {
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 18px 0;
|
||||
background: rgba(0, 209, 255, 0.03);
|
||||
}
|
||||
.marquee-track {
|
||||
display: flex;
|
||||
gap: 64px;
|
||||
width: max-content;
|
||||
animation: scroll 38s linear infinite;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.marquee-track span.sep { color: var(--cyan); }
|
||||
@keyframes scroll {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* Generic micro-bar (mini chart) */
|
||||
.spark {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
height: 32px;
|
||||
}
|
||||
.spark span {
|
||||
flex: 1;
|
||||
background: linear-gradient(180deg, var(--cyan), transparent);
|
||||
border-radius: 2px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Section title block */
|
||||
.section-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
margin-bottom: 64px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Two-column split */
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.15fr;
|
||||
gap: 80px;
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.split { grid-template-columns: 1fr; gap: 50px; }
|
||||
}
|
||||
|
||||
/* Scroll progress bar */
|
||||
.scroll-progress {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
height: 2px;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--cyan), var(--orange));
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 14px var(--cyan);
|
||||
transition: width 0.05s;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: rgba(0, 209, 255, 0.35);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 10px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, var(--cyan-deep), #1a3a5a);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* EDITORIAL / BLUEPRINT CHROME — v2 enhancements */
|
||||
/* ============================================================ */
|
||||
|
||||
/* Section frame with architectural dimension corners */
|
||||
.frame {
|
||||
position: relative;
|
||||
padding: 60px 56px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
background:
|
||||
radial-gradient(800px 400px at 30% -10%, rgba(0,209,255,0.08), transparent 70%),
|
||||
linear-gradient(180deg, rgba(6,12,24,0.55), rgba(2,6,14,0.35));
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.frame-tight { padding: 36px 36px; }
|
||||
|
||||
/* Engineering corner ticks — drawn with conic + line gradients */
|
||||
.frame::before, .frame::after,
|
||||
.frame-corners > .ct {
|
||||
position: absolute;
|
||||
width: 28px; height: 28px;
|
||||
border-color: var(--cyan);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Blueprint dimension marker (tl/tr/bl/br) — draws an L-shape with ticks */
|
||||
.dim-corner {
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dim-corner svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||||
.dim-corner.tl { top: -10px; left: -10px; }
|
||||
.dim-corner.tr { top: -10px; right: -10px; transform: scaleX(-1); }
|
||||
.dim-corner.bl { bottom: -10px; left: -10px; transform: scaleY(-1); }
|
||||
.dim-corner.br { bottom: -10px; right: -10px; transform: scale(-1, -1); }
|
||||
|
||||
/* Section number — oversized mono */
|
||||
.section-number {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.32em;
|
||||
color: var(--cyan-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.section-number .num {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--cyan);
|
||||
text-shadow: 0 0 12px rgba(0, 209, 255, 0.6);
|
||||
}
|
||||
.section-number::before {
|
||||
content: "";
|
||||
width: 36px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--cyan), transparent);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Vertical mono label running down the page edge */
|
||||
.vrail {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, transparent, var(--border), transparent);
|
||||
}
|
||||
.vrail-label {
|
||||
position: absolute;
|
||||
writing-mode: vertical-rl;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
top: 30%;
|
||||
left: -22px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Editorial display type — even larger, tighter */
|
||||
.h-mega {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(64px, 9.5vw, 148px);
|
||||
line-height: 0.88;
|
||||
letter-spacing: -0.045em;
|
||||
font-weight: 600;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.h-mega .grad {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #c1e8ff 55%, #5fc8ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.h-mega .accent { color: var(--cyan); }
|
||||
.h-mega .orange { color: var(--orange); }
|
||||
.h-mega .outline {
|
||||
-webkit-text-stroke: 1.2px var(--cyan);
|
||||
color: transparent;
|
||||
text-shadow: 0 0 30px rgba(0,209,255,0.35);
|
||||
}
|
||||
.h-mega .ital {
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Mono dimension marker tag (e.g. "18'-7"") */
|
||||
.dim-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--cyan-2);
|
||||
padding: 4px 10px;
|
||||
border: 1px dashed rgba(0, 209, 255, 0.45);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 209, 255, 0.04);
|
||||
}
|
||||
|
||||
/* Engineering tick line — used as section dividers */
|
||||
.tick-line {
|
||||
height: 18px;
|
||||
background-image:
|
||||
repeating-linear-gradient(90deg,
|
||||
var(--cyan) 0 1px, transparent 1px 24px);
|
||||
mask-image: linear-gradient(180deg, var(--cyan) 0 60%, transparent 60%);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Blueprint dimension callout used inside frames */
|
||||
.dim-callout {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--cyan-2);
|
||||
background: rgba(2, 6, 14, 0.85);
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(0, 209, 255, 0.3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* "Plate" badge — used for product spec labels */
|
||||
.plate {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(4, 12, 24, 0.65);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.plate .pnum {
|
||||
color: var(--cyan);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Floating system status pill (sticky) */
|
||||
.sys-pill {
|
||||
position: fixed;
|
||||
bottom: 22px; right: 22px;
|
||||
z-index: 40;
|
||||
padding: 10px 16px;
|
||||
background: rgba(2, 6, 14, 0.85);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
backdrop-filter: blur(10px);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 12px 30px -10px rgba(0,209,255,0.35);
|
||||
}
|
||||
.sys-pill .dot {
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 10px var(--green);
|
||||
animation: pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Section head v2 — split into rail + content */
|
||||
.shead {
|
||||
display: grid;
|
||||
grid-template-columns: 220px 1fr;
|
||||
gap: 60px;
|
||||
margin-bottom: 80px;
|
||||
align-items: start;
|
||||
}
|
||||
.shead .rail {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.shead .rail .num-big {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 80px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
background: linear-gradient(180deg, #00d1ff, rgba(0, 209, 255, 0.15));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.shead .rail .tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.shead .rail .ticks {
|
||||
width: 80px;
|
||||
height: 8px;
|
||||
background-image: repeating-linear-gradient(90deg, var(--cyan) 0 1px, transparent 1px 9px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.shead { grid-template-columns: 1fr; gap: 18px; margin-bottom: 50px; }
|
||||
.shead .rail { position: static; }
|
||||
.shead .rail .num-big { font-size: 54px; }
|
||||
}
|
||||
|
||||
/* "Ledger" / spec list */
|
||||
.ledger {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.ledger > .row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 22px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background 0.25s, border-color 0.25s;
|
||||
}
|
||||
.ledger > .row:hover {
|
||||
background: rgba(0, 209, 255, 0.03);
|
||||
border-bottom-color: var(--border-strong);
|
||||
}
|
||||
.ledger > .row .lnum {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--cyan-2);
|
||||
}
|
||||
|
||||
/* Hero specific tweaks (v2) */
|
||||
.hero-frame {
|
||||
position: relative;
|
||||
padding: 100px 56px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(700px 400px at 20% 0%, rgba(0,130,200,0.18), transparent 60%),
|
||||
radial-gradient(900px 600px at 100% 100%, rgba(0,209,255,0.12), transparent 65%),
|
||||
linear-gradient(180deg, rgba(4, 10, 22, 0.5), rgba(2, 6, 14, 0.2));
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.hero-frame { padding: 60px 24px; }
|
||||
}
|
||||
|
||||
/* Sharper card variant (for editorial sections) */
|
||||
.card-sharp {
|
||||
background: linear-gradient(180deg, rgba(8, 18, 36, 0.8), rgba(2, 8, 18, 0.7));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
position: relative;
|
||||
}
|
||||
.card-sharp.no-radius {
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Welcome to LynkedUp Pro — you're a Founding Member</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root { --orange:#ff9321; --blue:#4dc5ff; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; min-height: 100vh;
|
||||
font-family: "Plus Jakarta Sans", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
color: #fff;
|
||||
background:
|
||||
radial-gradient(circle at top left, #8c4a13 0%, transparent 38%),
|
||||
radial-gradient(circle at bottom right, #1597e5 0%, transparent 40%),
|
||||
#14100c;
|
||||
display: flex; align-items: center; justify-content: center; padding: 32px 18px;
|
||||
}
|
||||
.card {
|
||||
width: 100%; max-width: 620px;
|
||||
background: rgba(18,18,22,.72); border: 1px solid rgba(255,255,255,.10);
|
||||
border-radius: 22px; padding: 44px 40px; text-align: center;
|
||||
box-shadow: 0 30px 80px rgba(0,0,0,.5); backdrop-filter: blur(6px);
|
||||
}
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 7px 15px; border-radius: 999px; margin-bottom: 22px;
|
||||
background: linear-gradient(90deg, var(--orange), var(--blue));
|
||||
color: #0d0d0f; font-weight: 800; font-size: 12.5px; letter-spacing: .02em;
|
||||
}
|
||||
.check {
|
||||
width: 66px; height: 66px; margin: 0 auto 22px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--orange), var(--blue));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 34px; color: #0d0d0f; font-weight: 800;
|
||||
}
|
||||
h1 { margin: 0 0 10px; font-size: 27px; font-weight: 800; letter-spacing: -.02em; line-height: 1.2; }
|
||||
p.lead { margin: 0 auto 26px; max-width: 480px; color: #c9c9d2; font-size: 15px; line-height: 1.6; }
|
||||
.steps { text-align: left; margin: 0 auto 30px; max-width: 470px; display: grid; gap: 12px; }
|
||||
.step { display: flex; gap: 13px; align-items: flex-start; padding: 14px 16px;
|
||||
background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.07); border-radius: 13px; }
|
||||
.step .n { flex: 0 0 26px; height: 26px; border-radius: 50%;
|
||||
background: rgba(255,147,33,.16); color: var(--orange);
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px; }
|
||||
.step .t { font-size: 14px; line-height: 1.5; color: #e6e6ea; }
|
||||
.step .t b { color: #fff; }
|
||||
.cta { display: inline-block; margin-top: 6px; padding: 14px 30px; border-radius: 12px;
|
||||
text-decoration: none; color: #fff; font-weight: 700; font-size: 15px;
|
||||
background: linear-gradient(90deg, var(--orange), var(--blue)); box-shadow: 0 10px 25px rgba(0,0,0,.3); }
|
||||
.fine { margin-top: 22px; font-size: 12px; color: #8a8a93; line-height: 1.6; }
|
||||
.fine a { color: var(--blue); text-decoration: none; }
|
||||
.demo-note { margin: -6px auto 24px; max-width: 470px; padding: 11px 15px; border-radius: 12px;
|
||||
background: rgba(77,197,255,.12); border: 1px solid rgba(77,197,255,.32); color: #bfe6ff; font-size: 13px; line-height: 1.55; }
|
||||
.ref { margin-top: 10px; font-size: 11px; color: #6f6f78; word-break: break-all; }
|
||||
@media (max-width: 560px) { .card { padding: 34px 22px; } h1 { font-size: 23px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="check">✓</div>
|
||||
<span class="badge" id="badge">🎉 You're a Founding Member</span>
|
||||
<h1 id="headline">Payment received — welcome to LynkedUp Pro</h1>
|
||||
<p class="lead" id="lead">
|
||||
Your Founders Lifetime License is locked in at the founder price. A confirmation
|
||||
email is on its way, and our onboarding team will reach out shortly to get your
|
||||
account set up.
|
||||
</p>
|
||||
<p class="demo-note" id="demoNote" hidden>
|
||||
🎬 <b>Demo mode.</b> This was a walkthrough of the full flow — no payment was taken
|
||||
and no card was charged.
|
||||
</p>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step"><span class="n">1</span><span class="t"><b>Check your inbox.</b> Your receipt and a welcome email with next steps are on the way. (Peek in spam if you don't see them in a few minutes.)</span></div>
|
||||
<div class="step"><span class="n">2</span><span class="t"><b>We'll set you up.</b> Our team will contact you at the details you provided to activate your license and walk you through onboarding.</span></div>
|
||||
<div class="step"><span class="n">3</span><span class="t"><b>Watch for launch updates.</b> As a Founding Member you'll hear first about new features and your rollout timeline.</span></div>
|
||||
</div>
|
||||
|
||||
<a class="cta" id="setupCta" href="/portal/register">Set up your account</a>
|
||||
<p class="fine" id="setupStatus" style="margin-top:10px;">Preparing your account…</p>
|
||||
|
||||
<p class="fine">
|
||||
Questions? Email <a href="mailto:support@lynkeduppro.com">support@lynkeduppro.com</a> and reference your order below.
|
||||
</p>
|
||||
<p class="ref" id="ref"></p>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var q = new URLSearchParams(location.search);
|
||||
var demo = q.get("demo") === "1";
|
||||
// Demo mode ($0 no-card flow from page/19.html): reframe the page so nobody
|
||||
// thinks a real charge happened.
|
||||
if (demo) {
|
||||
document.getElementById("badge").textContent = "🎬 Demo complete";
|
||||
document.getElementById("headline").innerHTML = "That's the full flow — start to finish";
|
||||
document.getElementById("lead").textContent =
|
||||
"You just walked through the entire Founders License journey. On the live page this is where the real payment would complete.";
|
||||
document.getElementById("demoNote").hidden = false;
|
||||
}
|
||||
// Show the Stripe session id (from success_url) so support can look up the order.
|
||||
var id = q.get("session_id");
|
||||
if (id) document.getElementById("ref").textContent = "Order reference: " + id;
|
||||
|
||||
var status = document.getElementById("setupStatus");
|
||||
var cta = document.getElementById("setupCta");
|
||||
if (demo || !id) {
|
||||
// No real purchase to attribute — just offer the register page.
|
||||
if (status) status.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll be-crm (via our proxy) for the founder token minted by the Stripe
|
||||
// webhook, then redirect to the prefilled register page. The welcome email
|
||||
// carries the same link, so this is a convenience — nothing is lost if the
|
||||
// webhook is briefly delayed.
|
||||
var tries = 0, MAX = 15; // ~30s at 2s intervals
|
||||
function poll() {
|
||||
tries++;
|
||||
fetch("/api/founder/session?session_id=" + encodeURIComponent(id), { cache: "no-store" })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d && d.status === "ready" && d.token) {
|
||||
var url = "/portal/register?ft=" + encodeURIComponent(d.token);
|
||||
if (cta) cta.setAttribute("href", url);
|
||||
if (status) status.textContent = "Redirecting you to finish setup…";
|
||||
window.location.replace(url);
|
||||
return;
|
||||
}
|
||||
if (tries < MAX) { setTimeout(poll, 2000); return; }
|
||||
if (status) status.innerHTML = "We emailed you a secure link to finish setting up your account — check your inbox (and spam).";
|
||||
})
|
||||
.catch(function () {
|
||||
if (tries < MAX) { setTimeout(poll, 2000); return; }
|
||||
if (status) status.innerHTML = "We emailed you a secure link to finish setting up your account — check your inbox (and spam).";
|
||||
});
|
||||
}
|
||||
poll();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,431 @@
|
||||
/* global React */
|
||||
// Shared UI primitives for LynkedUp Pro
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
// ---------- Hex logo (matches flyer mark) ----------
|
||||
function Logo({ size = 90 }) {
|
||||
return (
|
||||
<div className="row gap-12" style={{ alignItems: "center" }}>
|
||||
<img
|
||||
src="uploads/lynkedup-logo.png"
|
||||
alt="LynkedUp Pro"
|
||||
width={150}
|
||||
height={30}
|
||||
style={{ objectFit: "contain", filter: "drop-shadow(0 0 8px rgba(0,209,255,0.6))" }}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Counter that animates on view ----------
|
||||
function Counter({ to, suffix = "", prefix = "", duration = 1800, decimals = 0 }) {
|
||||
const ref = useRef(null);
|
||||
const [val, setVal] = useState(0);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const obs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
const start = performance.now();
|
||||
const tick = (now) => {
|
||||
const t = Math.min(1, (now - start) / duration);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
setVal(to * ease);
|
||||
if (t < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
obs.disconnect();
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, [to]);
|
||||
const formatted = decimals
|
||||
? val.toFixed(decimals)
|
||||
: Math.round(val).toLocaleString();
|
||||
return <span ref={ref} className="tabular">{prefix}{formatted}{suffix}</span>;
|
||||
}
|
||||
|
||||
// ---------- Reveal-on-scroll wrapper ----------
|
||||
function Reveal({ children, delay = 0, className = "", as: As = "div", style = {} }) {
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const obs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
el.classList.add("in");
|
||||
obs.unobserve(el);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: "0px 0px -60px 0px" });
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
const delayCls = delay ? ` delay-${delay}` : "";
|
||||
return (
|
||||
<As ref={ref} className={`reveal${delayCls} ${className}`} style={style}>
|
||||
{children}
|
||||
</As>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Card with mouse-tracking glow ----------
|
||||
function GlowCard({ children, className = "", style = {}, onClick }) {
|
||||
const ref = useRef(null);
|
||||
const handle = (e) => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
el.style.setProperty("--mx", `${e.clientX - r.left}px`);
|
||||
el.style.setProperty("--my", `${e.clientY - r.top}px`);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`card ${className}`}
|
||||
style={style}
|
||||
onMouseMove={handle}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="corner tl"></span>
|
||||
<span className="corner tr"></span>
|
||||
<span className="corner bl"></span>
|
||||
<span className="corner br"></span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Wireframe Blueprint House (SVG, animated drawing) ----------
|
||||
function BlueprintHouse({ size = 600, animate = true, withMeasurements = true, withScan = true }) {
|
||||
// viewBox 600x460
|
||||
const dash = animate ? { strokeDasharray: 1400, strokeDashoffset: 1400, animation: "drawIn 4s ease-out forwards" } : {};
|
||||
return (
|
||||
<svg viewBox="0 0 600 460" width={size} height={size * 460 / 600} style={{ display: "block", overflow: "visible" }}>
|
||||
<defs>
|
||||
<linearGradient id="hglow" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0" stopColor="#7be8ff"/>
|
||||
<stop offset="1" stopColor="#0090c0"/>
|
||||
</linearGradient>
|
||||
<filter id="bglow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="b"/>
|
||||
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
<pattern id="floorgrid" width="32" height="32" patternUnits="userSpaceOnUse" patternTransform="skewX(-30) skewY(0)">
|
||||
<path d="M32 0 L0 0 L0 32" fill="none" stroke="rgba(0,209,255,0.22)" strokeWidth="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{/* Floor grid */}
|
||||
<g transform="translate(0,380)">
|
||||
<rect x="-60" y="0" width="720" height="80" fill="url(#floorgrid)" opacity="0.7"/>
|
||||
</g>
|
||||
|
||||
{/* House outline - two-gable */}
|
||||
<g stroke="url(#hglow)" strokeWidth="1.5" fill="none" filter="url(#bglow)" style={dash}>
|
||||
{/* Main body */}
|
||||
<path d="M60 380 L60 200 L210 90 L360 200 L360 380 Z" />
|
||||
{/* Right side smaller gable */}
|
||||
<path d="M360 380 L360 240 L450 180 L540 240 L540 380 Z" />
|
||||
{/* Roof ridge */}
|
||||
<line x1="210" y1="90" x2="210" y2="200" opacity="0.4"/>
|
||||
<line x1="450" y1="180" x2="450" y2="240" opacity="0.4"/>
|
||||
{/* Chimney */}
|
||||
<rect x="100" y="135" width="22" height="48"/>
|
||||
{/* Door */}
|
||||
<rect x="180" y="290" width="60" height="90"/>
|
||||
<circle cx="232" cy="335" r="2.5" fill="#7be8ff"/>
|
||||
{/* Windows */}
|
||||
<rect x="90" y="240" width="60" height="50"/>
|
||||
<line x1="120" y1="240" x2="120" y2="290" opacity="0.5"/>
|
||||
<line x1="90" y1="265" x2="150" y2="265" opacity="0.5"/>
|
||||
<rect x="270" y="240" width="60" height="50"/>
|
||||
<line x1="300" y1="240" x2="300" y2="290" opacity="0.5"/>
|
||||
<line x1="270" y1="265" x2="330" y2="265" opacity="0.5"/>
|
||||
<rect x="410" y="280" width="80" height="60"/>
|
||||
<line x1="450" y1="280" x2="450" y2="340" opacity="0.5"/>
|
||||
<line x1="410" y1="310" x2="490" y2="310" opacity="0.5"/>
|
||||
{/* Roof structure lines */}
|
||||
<line x1="60" y1="200" x2="360" y2="200" opacity="0.35"/>
|
||||
<line x1="360" y1="240" x2="540" y2="240" opacity="0.35"/>
|
||||
</g>
|
||||
|
||||
{/* Measurements */}
|
||||
{withMeasurements && (
|
||||
<g fontFamily="var(--font-mono)" fontSize="13" fill="#7be8ff" filter="url(#bglow)">
|
||||
<g>
|
||||
<line x1="60" y1="60" x2="210" y2="60" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
|
||||
<text x="115" y="52">18′-7″</text>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="210" y1="60" x2="360" y2="60" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
|
||||
<text x="265" y="52">12′-3″</text>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="360" y1="160" x2="540" y2="160" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
|
||||
<text x="430" y="153">15′-4″</text>
|
||||
</g>
|
||||
<g>
|
||||
<line x1="560" y1="240" x2="560" y2="380" stroke="#7be8ff" strokeWidth="0.8" strokeDasharray="3 3" opacity="0.7"/>
|
||||
<text x="566" y="315">8′-0″</text>
|
||||
</g>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Corner markers */}
|
||||
<g fill="#00d1ff">
|
||||
{[[60,200],[210,90],[360,200],[450,180],[540,240]].map(([x,y],i) => (
|
||||
<g key={i}>
|
||||
<circle cx={x} cy={y} r="3.5" />
|
||||
<circle cx={x} cy={y} r="9" fill="none" stroke="#00d1ff" opacity="0.5">
|
||||
<animate attributeName="r" values="6;14;6" dur="2.4s" begin={`${i*0.2}s`} repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.7;0;0.7" dur="2.4s" begin={`${i*0.2}s`} repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
))}
|
||||
</g>
|
||||
|
||||
{/* AI scan line */}
|
||||
{withScan && (
|
||||
<rect x="0" y="80" width="600" height="3" fill="rgba(0,209,255,0.7)" style={{ filter: "blur(2px)" }}>
|
||||
<animate attributeName="y" values="80;380;80" dur="6s" repeatCount="indefinite" />
|
||||
<animate attributeName="opacity" values="0;1;0" dur="6s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Tiny chart components ----------
|
||||
function Sparkline({ data, color = "#00d1ff", height = 36, width = 120 }) {
|
||||
const max = Math.max(...data), min = Math.min(...data);
|
||||
const range = max - min || 1;
|
||||
const stepX = width / (data.length - 1);
|
||||
const pts = data.map((v, i) => `${i * stepX},${height - ((v - min) / range) * height}`).join(" ");
|
||||
const area = `0,${height} ${pts} ${width},${height}`;
|
||||
return (
|
||||
<svg width={width} height={height} style={{ display: "block" }}>
|
||||
<defs>
|
||||
<linearGradient id={`sg-${color}`} x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0" stopColor={color} stopOpacity="0.4"/>
|
||||
<stop offset="1" stopColor={color} stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points={area} fill={`url(#sg-${color})`} />
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.6"
|
||||
style={{ filter: `drop-shadow(0 0 6px ${color})` }}/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function DonutChart({ size = 120, stroke = 12, segments }) {
|
||||
// segments: [{value, color}]
|
||||
const r = (size - stroke) / 2;
|
||||
const c = 2 * Math.PI * r;
|
||||
const total = segments.reduce((s, x) => s + x.value, 0);
|
||||
let offset = 0;
|
||||
return (
|
||||
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
|
||||
<circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth={stroke}/>
|
||||
{segments.map((s, i) => {
|
||||
const len = (s.value / total) * c;
|
||||
const el = (
|
||||
<circle key={i} cx={size/2} cy={size/2} r={r}
|
||||
fill="none"
|
||||
stroke={s.color}
|
||||
strokeWidth={stroke}
|
||||
strokeDasharray={`${len} ${c - len}`}
|
||||
strokeDashoffset={-offset}
|
||||
style={{ filter: `drop-shadow(0 0 4px ${s.color})` }}
|
||||
/>
|
||||
);
|
||||
offset += len;
|
||||
return el;
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Icon set (line, glowing) ----------
|
||||
const Icon = ({ name, size = 22, stroke = "#7be8ff" }) => {
|
||||
const props = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke, strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", style: { filter: `drop-shadow(0 0 6px ${stroke}55)` } };
|
||||
switch (name) {
|
||||
case "ai":
|
||||
return <svg {...props}><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/><circle cx="12" cy="12" r="4"/><path d="M10 12h4M12 10v4"/></svg>;
|
||||
case "calendar":
|
||||
return <svg {...props}><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/><rect x="7" y="13" width="4" height="3" rx="0.5"/></svg>;
|
||||
case "crew":
|
||||
return <svg {...props}><circle cx="9" cy="8" r="3"/><circle cx="17" cy="10" r="2.2"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6M14 20c0-2 1.2-3.6 3-4.4"/></svg>;
|
||||
case "fleet":
|
||||
return <svg {...props}><circle cx="12" cy="11" r="9"/><path d="M3 11h18M12 2c3 3 3 15 0 18M12 2c-3 3-3 15 0 18"/><circle cx="12" cy="11" r="2" fill={stroke}/></svg>;
|
||||
case "budget":
|
||||
return <svg {...props}><path d="M3 17l5-5 4 3 8-9"/><path d="M14 6h6v6"/><path d="M3 21h18"/></svg>;
|
||||
case "report":
|
||||
return <svg {...props}><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>;
|
||||
case "chat":
|
||||
return <svg {...props}><path d="M21 12a8 8 0 1 1-3.4-6.5L21 4l-1.4 3.5A8 8 0 0 1 21 12z"/><path d="M8 11h.01M12 11h.01M16 11h.01"/></svg>;
|
||||
case "field":
|
||||
return <svg {...props}><path d="M3 12l9-8 9 8M5 10v10h14V10"/><path d="M10 20v-6h4v6"/></svg>;
|
||||
case "shield":
|
||||
return <svg {...props}><path d="M12 3l8 3v6c0 5-4 8-8 9-4-1-8-4-8-9V6l8-3z"/><path d="M9 12l2 2 4-4"/></svg>;
|
||||
case "clock":
|
||||
return <svg {...props}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>;
|
||||
case "target":
|
||||
return <svg {...props}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill={stroke}/></svg>;
|
||||
case "growth":
|
||||
return <svg {...props}><path d="M3 20V8M9 20V13M15 20v-9M21 20V5"/></svg>;
|
||||
case "bell":
|
||||
return <svg {...props}><path d="M18 16v-5a6 6 0 0 0-12 0v5l-2 3h16l-2-3z"/><path d="M10 21a2 2 0 0 0 4 0"/></svg>;
|
||||
case "pin":
|
||||
return <svg {...props}><path d="M12 21s7-6 7-12a7 7 0 0 0-14 0c0 6 7 12 7 12z"/><circle cx="12" cy="9" r="2.5"/></svg>;
|
||||
case "sync":
|
||||
return <svg {...props}><path d="M21 12a9 9 0 0 1-15.5 6.3M3 12a9 9 0 0 1 15.5-6.3"/><path d="M21 4v5h-5M3 20v-5h5"/></svg>;
|
||||
case "brain":
|
||||
return <svg {...props}><path d="M9 4a3 3 0 0 0-3 3v0a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3v0a3 3 0 0 0 3 3"/><path d="M15 4a3 3 0 0 1 3 3v0a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v0a3 3 0 0 1-3 3"/><path d="M12 4v16M9 9h6M9 14h6"/></svg>;
|
||||
case "doc":
|
||||
return <svg {...props}><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/></svg>;
|
||||
case "camera":
|
||||
return <svg {...props}><path d="M4 8h3l2-3h6l2 3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z"/><circle cx="12" cy="13" r="4"/></svg>;
|
||||
case "hammer":
|
||||
return <svg {...props}><path d="M15 5l4 4-3 3-4-4 3-3z"/><path d="M12 8L4 16l3 3 8-8"/></svg>;
|
||||
case "lead":
|
||||
return <svg {...props}><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/><path d="M16 4l2 2-4 4"/></svg>;
|
||||
case "money":
|
||||
return <svg {...props}><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M5 9v.01M19 15v.01"/></svg>;
|
||||
case "spark":
|
||||
return <svg {...props}><path d="M12 3l1.7 5.3L19 10l-5.3 1.7L12 17l-1.7-5.3L5 10l5.3-1.7L12 3z"/></svg>;
|
||||
case "arrow":
|
||||
return <svg {...props}><path d="M5 12h14M13 5l7 7-7 7"/></svg>;
|
||||
case "play":
|
||||
return <svg {...props}><polygon points="6,4 20,12 6,20" fill={stroke} opacity="0.2"/><polygon points="6,4 20,12 6,20"/></svg>;
|
||||
default:
|
||||
return <svg {...props}><circle cx="12" cy="12" r="9"/></svg>;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- Dimension corner brackets (blueprint chrome) ----------
|
||||
function DimCorner({ pos = "tl" }) {
|
||||
return (
|
||||
<div className={`dim-corner ${pos}`}>
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path d="M0 14 L0 0 L14 0" fill="none" stroke="#00d1ff" strokeWidth="1.2"
|
||||
style={{ filter: "drop-shadow(0 0 4px #00d1ff)" }}/>
|
||||
<line x1="0" y1="6" x2="6" y2="6" stroke="#00d1ff" strokeWidth="0.8" opacity="0.6"/>
|
||||
<line x1="6" y1="0" x2="6" y2="6" stroke="#00d1ff" strokeWidth="0.8" opacity="0.6"/>
|
||||
<circle cx="0" cy="0" r="2" fill="#00d1ff"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Dimension line (engineering measurement) ----------
|
||||
function DimLine({ orientation = "h", label, length = "100%", offset = 0, side = "top" }) {
|
||||
const isH = orientation === "h";
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
[side]: offset,
|
||||
...(isH ? { left: "10%", right: "10%" } : { top: "10%", bottom: "10%", width: 20 }),
|
||||
pointerEvents: "none",
|
||||
}}>
|
||||
<svg viewBox={isH ? "0 0 200 12" : "0 0 12 200"}
|
||||
preserveAspectRatio="none"
|
||||
style={{ width: isH ? "100%" : 12, height: isH ? 12 : "100%" }}>
|
||||
{isH ? (
|
||||
<>
|
||||
<line x1="0" y1="6" x2="200" y2="6" stroke="#7be8ff" strokeWidth="0.6" strokeDasharray="3 3" opacity="0.6"/>
|
||||
<line x1="0" y1="2" x2="0" y2="10" stroke="#7be8ff" strokeWidth="0.8"/>
|
||||
<line x1="200" y1="2" x2="200" y2="10" stroke="#7be8ff" strokeWidth="0.8"/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="6" y1="0" x2="6" y2="200" stroke="#7be8ff" strokeWidth="0.6" strokeDasharray="3 3" opacity="0.6"/>
|
||||
<line x1="2" y1="0" x2="10" y2="0" stroke="#7be8ff" strokeWidth="0.8"/>
|
||||
<line x1="2" y1="200" x2="10" y2="200" stroke="#7be8ff" strokeWidth="0.8"/>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
{label && (
|
||||
<span style={{
|
||||
position: "absolute",
|
||||
padding: "2px 6px",
|
||||
background: "rgba(2,6,14,0.9)",
|
||||
border: "1px solid rgba(0,209,255,0.3)",
|
||||
borderRadius: 3,
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.16em",
|
||||
color: "var(--cyan-2)",
|
||||
}}>{label}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- SectionFrame: standard architectural-framed section ----------
|
||||
function SectionFrame({ children, number, tag, vlabel, id, className = "", style = {} }) {
|
||||
return (
|
||||
<section id={id} className={`section ${className}`} style={style}>
|
||||
<div className="shell" style={{ position: "relative" }}>
|
||||
{vlabel && (
|
||||
<>
|
||||
<div className="vrail"/>
|
||||
<div className="vrail-label">{vlabel}</div>
|
||||
</>
|
||||
)}
|
||||
{(number || tag) && (
|
||||
<Reveal>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 40 }}>
|
||||
{number && (
|
||||
<span className="section-number">
|
||||
<span className="num">§{number}</span>
|
||||
<span>{tag}</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="dim-tag">// {vlabel || "BUILD MK.07"}</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Section head with rail (number + tag + ticks) ----------
|
||||
function SectionHead({ number, tag, title, sub, align = "left" }) {
|
||||
return (
|
||||
<div className="shead">
|
||||
<Reveal>
|
||||
<div className="rail">
|
||||
<span className="num-big">{number}</span>
|
||||
<span className="tag">{tag}</span>
|
||||
<div className="ticks"/>
|
||||
</div>
|
||||
</Reveal>
|
||||
<div>
|
||||
<Reveal delay={1}>
|
||||
{typeof title === "string" ? <h2 className="h-section">{title}</h2> : title}
|
||||
</Reveal>
|
||||
{sub && (
|
||||
<Reveal delay={2}>
|
||||
<p className="sub" style={{ marginTop: 20 }}>{sub}</p>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export to window for cross-script access
|
||||
Object.assign(window, { Logo, Counter, Reveal, GlowCard, BlueprintHouse, Sparkline, DonutChart, Icon, DimCorner, DimLine, SectionFrame, SectionHead });
|
||||
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 215 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,129 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateLead, leadToStripeMetadata, SELF_SERVE_MAX } from "@/lib/founder/lead";
|
||||
|
||||
// Validates the buyer info captured by the pre-checkout form, then creates a
|
||||
// Stripe Checkout Session and returns its hosted-checkout URL. The Stripe secret
|
||||
// key stays server-side and is NEVER exposed to the browser. Ported from the
|
||||
// landing's api/checkout.js.
|
||||
//
|
||||
// Env:
|
||||
// STRIPE_SECRET_KEY (required) sk_test_… / sk_live_… — swap to switch modes
|
||||
// CHECKOUT_PRICE cents, default 200000 (USD 2000.00)
|
||||
// CHECKOUT_CURRENCY ISO code, default usd
|
||||
// CHECKOUT_PRODUCT_NAME line-item name, default "Founders Lifetime Access"
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
// Bump when the SMS consent wording in public/square-checkout.js changes (TCPA evidence).
|
||||
const SMS_CONSENT_VERSION = "v1";
|
||||
// Server-side promo table — the client can only pass a code name, never a price.
|
||||
const PROMOS: Record<string, number> = { SAVE30: 30, SAVE50: 50, SAVE70: 70, SAVE99: 99, JUSTIN99: 99 };
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const secretKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
return NextResponse.json({ error: "Missing STRIPE_SECRET_KEY environment variable on the server." }, { status: 500 });
|
||||
}
|
||||
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await request.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
|
||||
// Never trust the browser: the form validates for UX, this validates for real.
|
||||
const result = validateLead(body);
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ error: "Please check the highlighted fields.", fields: result.errors }, { status: 400 });
|
||||
}
|
||||
const lead = result.lead;
|
||||
|
||||
// Orders over the self-serve limit are NOT payable online — they go to sales.
|
||||
if (lead.licenses > SELF_SERVE_MAX) {
|
||||
return NextResponse.json(
|
||||
{ error: `Orders of more than ${SELF_SERVE_MAX} licenses are handled by our sales team.`, path: "sales", licenses: lead.licenses },
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const origin = originFromRequest(request, url);
|
||||
|
||||
let amount = parseInt(process.env.CHECKOUT_PRICE || "200000", 10); // USD 2000.00 in cents
|
||||
let currency = (process.env.CHECKOUT_CURRENCY || "usd").toLowerCase();
|
||||
let productName = process.env.CHECKOUT_PRODUCT_NAME || "Founders Lifetime Access";
|
||||
|
||||
// Optional $1 demo plan (?plan=demo) — the amount is fixed server-side.
|
||||
const plan = url.searchParams.get("plan");
|
||||
if (plan === "demo") {
|
||||
amount = 100;
|
||||
currency = "usd";
|
||||
productName = "LynkedUp Pro Demo Access ($1)";
|
||||
}
|
||||
|
||||
// Promo codes (?promo=CODE) — percent-off applied here, never trusting a client price.
|
||||
const promoCode = (url.searchParams.get("promo") || "").trim().toUpperCase();
|
||||
let appliedPct = 0;
|
||||
if (plan !== "demo" && promoCode && Object.prototype.hasOwnProperty.call(PROMOS, promoCode)) {
|
||||
appliedPct = PROMOS[promoCode];
|
||||
amount = Math.max(50, Math.round((amount * (100 - appliedPct)) / 100)); // Stripe min charge $0.50
|
||||
productName = `${productName} (${appliedPct}% off · code ${promoCode})`;
|
||||
}
|
||||
|
||||
const consentIp = String(request.headers.get("x-forwarded-for") || "").split(",")[0].trim();
|
||||
const capturedAt = new Date().toISOString();
|
||||
// Let Stripe multiply via quantity so the Dashboard shows "N x $X". Demo is a single unit.
|
||||
const quantity = plan === "demo" ? 1 : lead.licenses;
|
||||
|
||||
const metadata = leadToStripeMetadata(lead, {
|
||||
sms_consent_version: SMS_CONSENT_VERSION,
|
||||
sms_consent_at: lead.smsConsent ? capturedAt : "",
|
||||
sms_consent_ip: consentIp,
|
||||
plan: plan || "founders",
|
||||
promo_code: appliedPct ? promoCode : "",
|
||||
unit_amount_cents: String(amount),
|
||||
total_amount_cents: String(amount * quantity),
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({
|
||||
mode: "payment",
|
||||
success_url: `${origin}/thanks?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${origin}/?canceled=1`,
|
||||
"line_items[0][quantity]": String(quantity),
|
||||
"line_items[0][price_data][currency]": currency,
|
||||
"line_items[0][price_data][unit_amount]": String(amount),
|
||||
"line_items[0][price_data][product_data][name]": productName,
|
||||
customer_email: lead.email,
|
||||
customer_creation: "always",
|
||||
billing_address_collection: "required",
|
||||
});
|
||||
// Mirror onto the Session (drives our webhook) AND the PaymentIntent (visible in Dashboard).
|
||||
for (const [k, v] of Object.entries(metadata)) {
|
||||
params.set(`metadata[${k}]`, String(v ?? ""));
|
||||
params.set(`payment_intent_data[metadata][${k}]`, String(v ?? ""));
|
||||
}
|
||||
|
||||
const r = await fetch("https://api.stripe.com/v1/checkout/sessions", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params.toString(),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) return NextResponse.json({ error: "Stripe checkout creation failed", detail: data }, { status: 502 });
|
||||
if (!data.url) return NextResponse.json({ error: "Stripe did not return a checkout URL", detail: data }, { status: 502 });
|
||||
|
||||
return NextResponse.json({ url: data.url });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: String((e as Error)?.message || e) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Build the public origin from the incoming request so success/cancel redirects
|
||||
// work on any deployment (preview or production).
|
||||
function originFromRequest(request: Request, url: URL): string {
|
||||
const proto = (request.headers.get("x-forwarded-proto") || url.protocol.replace(":", "") || "https").split(",")[0];
|
||||
const host = request.headers.get("x-forwarded-host") || request.headers.get("host") || url.host;
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// Server-side proxy to be-crm's public founder-access lookup, so the register page
|
||||
// can prefill the paid founder's details (and confirm the token is still valid)
|
||||
// before they have a session. Server-to-server avoids CORS. The token is the only
|
||||
// credential — it was emailed to the founder / carried from /thanks.
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const CRM_BASE_URL = (process.env.CRM_BASE_URL ?? "https://crm.lynkedup.cloud").replace(/\/$/, "");
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const token = new URL(request.url).searchParams.get("token") ?? "";
|
||||
if (token.length < 16 || token.length > 256) {
|
||||
return NextResponse.json({ ok: false, status: "invalid" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${CRM_BASE_URL}/public/founder-access/lookup?token=${encodeURIComponent(token)}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) return NextResponse.json({ ok: false, status: "unavailable" }, { status: 502 });
|
||||
return NextResponse.json(await res.json());
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, status: "unavailable" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
// Server-side proxy to be-crm's founder-access by-session lookup. The /thanks page
|
||||
// (post-Stripe) polls this with the checkout session_id until the webhook has
|
||||
// fulfilled the purchase and a founder token exists, then redirects to the
|
||||
// prefilled register page. `pending` means "poll again shortly".
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const CRM_BASE_URL = (process.env.CRM_BASE_URL ?? "https://crm.lynkedup.cloud").replace(/\/$/, "");
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const sessionId = new URL(request.url).searchParams.get("session_id") ?? "";
|
||||
if (!sessionId || sessionId.length > 256) {
|
||||
return NextResponse.json({ ok: false, status: "pending" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${CRM_BASE_URL}/public/founder-access/by-session?session_id=${encodeURIComponent(sessionId)}`, {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) return NextResponse.json({ ok: false, status: "pending" }, { status: 502 });
|
||||
return NextResponse.json(await res.json());
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, status: "pending" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateLead, SELF_SERVE_MAX } from "@/lib/founder/lead";
|
||||
|
||||
// Step-1 record capture for the founder-access form, BEFORE the payment branch.
|
||||
// Validates the same required fields as checkout and tells the client which path
|
||||
// to take by license count (<=SELF_SERVE_MAX -> Stripe checkout, else -> sales).
|
||||
// No payment happens here; the authoritative "paid" fulfillment is the Stripe
|
||||
// webhook -> be-crm. Ported from the landing's api/register.js.
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
body = (await request.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
|
||||
const result = validateLead(body);
|
||||
if (!result.ok) {
|
||||
return NextResponse.json({ error: "Please check the highlighted fields.", fields: result.errors }, { status: 400 });
|
||||
}
|
||||
|
||||
const lead = result.lead;
|
||||
const path = lead.licenses > SELF_SERVE_MAX ? "sales" : "checkout";
|
||||
return NextResponse.json({ ok: true, path, licenses: lead.licenses });
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
// Stripe webhook — the ONLY trustworthy signal that money actually moved. On a
|
||||
// paid Founders-License checkout it hands the purchase to be-crm, which mints the
|
||||
// founder token, sends the welcome email with the register link, and marks the
|
||||
// email owner-eligible. Ported from the landing's api/stripe-webhook.mjs (which
|
||||
// pushed to LeadPulse); here the fulfilment target is be-crm.
|
||||
//
|
||||
// Env:
|
||||
// STRIPE_WEBHOOK_SECRET whsec_… (Stripe Dashboard -> Webhooks -> signing secret)
|
||||
// CRM_BASE_URL be-crm base (default https://crm.lynkedup.cloud)
|
||||
// FOUNDER_FULFILL_SECRET shared secret sent to be-crm's public fulfill endpoint
|
||||
// Stripe endpoint URL: https://<domain>/api/stripe-webhook
|
||||
// Events: checkout.session.completed (+ checkout.session.async_payment_succeeded)
|
||||
//
|
||||
// Route Handlers receive the exact raw bytes via request.arrayBuffer() (no body
|
||||
// parser), which the signature check requires. Node runtime for node:crypto.
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const TOLERANCE_SECONDS = 300; // Stripe's documented replay window
|
||||
const CRM_BASE_URL = (process.env.CRM_BASE_URL ?? "https://crm.lynkedup.cloud").replace(/\/$/, "");
|
||||
|
||||
interface StripeSession {
|
||||
id: string;
|
||||
payment_status?: string;
|
||||
customer?: string | null;
|
||||
payment_intent?: string | null;
|
||||
amount_total?: number;
|
||||
currency?: string;
|
||||
created?: number;
|
||||
metadata?: Record<string, string>;
|
||||
customer_details?: { email?: string; address?: unknown } | null;
|
||||
}
|
||||
|
||||
function verifyStripeSignature(raw: Buffer, header: string | null, secret: string): { type: string; data: { object: StripeSession } } {
|
||||
if (!header) throw new Error("Missing Stripe-Signature header");
|
||||
let timestamp: string | null = null;
|
||||
const v1: string[] = [];
|
||||
for (const part of header.split(",")) {
|
||||
const i = part.indexOf("=");
|
||||
if (i === -1) continue;
|
||||
const key = part.slice(0, i).trim();
|
||||
const value = part.slice(i + 1).trim();
|
||||
if (key === "t") timestamp = value;
|
||||
else if (key === "v1") v1.push(value); // only v1 — ignoring v0 prevents a downgrade attack
|
||||
}
|
||||
if (!timestamp || !/^\d+$/.test(timestamp) || !v1.length) throw new Error("Malformed Stripe-Signature header");
|
||||
|
||||
const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), raw]);
|
||||
const expected = crypto.createHmac("sha256", secret).update(signedPayload).digest();
|
||||
const matched = v1.some((sig) => {
|
||||
if (!/^[a-f0-9]{64}$/i.test(sig)) return false;
|
||||
const got = Buffer.from(sig, "hex");
|
||||
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
|
||||
});
|
||||
if (!matched) throw new Error("No matching v1 signature");
|
||||
|
||||
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
|
||||
if (Math.abs(age) > TOLERANCE_SECONDS) throw new Error(`Timestamp outside tolerance (age ${age}s)`);
|
||||
|
||||
return JSON.parse(raw.toString("utf8"));
|
||||
}
|
||||
|
||||
async function fulfil(session: StripeSession): Promise<void> {
|
||||
// Card payments arrive already paid; delayed methods can complete a session while
|
||||
// still unpaid. Only a paid session is a real purchase.
|
||||
if (session.payment_status !== "paid") {
|
||||
console.log("[webhook] session complete but not paid — ignoring", { id: session.id, status: session.payment_status });
|
||||
return;
|
||||
}
|
||||
const md = session.metadata || {};
|
||||
const email = (md.email || session.customer_details?.email || "").toLowerCase();
|
||||
if (!email) {
|
||||
console.error("[webhook] paid session with no email — cannot attribute", { id: session.id });
|
||||
return;
|
||||
}
|
||||
|
||||
const secret = process.env.FOUNDER_FULFILL_SECRET;
|
||||
if (!secret) {
|
||||
// Throw so Stripe retries once the secret is configured (nothing is lost — the
|
||||
// session metadata replays the whole purchase on retry).
|
||||
throw new Error("FOUNDER_FULFILL_SECRET is not set");
|
||||
}
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
firstName: md.first_name || "",
|
||||
lastName: md.last_name || "",
|
||||
contactName: md.contact_name || "",
|
||||
companyName: md.company_name || "",
|
||||
phone: md.phone || "",
|
||||
streetAddress: md.street_address || "",
|
||||
city: md.city || "",
|
||||
state: md.state || "",
|
||||
zip: md.zip || "",
|
||||
businessAddress: md.business_address || "",
|
||||
salesReps: md.sales_reps || "",
|
||||
licenses: Number(md.num_licenses) || 1,
|
||||
plan: md.plan || "founders",
|
||||
promoCode: md.promo_code || null,
|
||||
smsConsent: md.sms_consent === "true",
|
||||
smsConsentVersion: md.sms_consent_version || null,
|
||||
smsConsentAt: md.sms_consent_at || null,
|
||||
stripeSessionId: session.id,
|
||||
stripeCustomerId: session.customer || null,
|
||||
stripePaymentIntent: session.payment_intent || null,
|
||||
amountTotalCents: session.amount_total ?? null,
|
||||
currency: session.currency || null,
|
||||
billingAddress: session.customer_details?.address ?? null,
|
||||
purchasedAt: session.created ? new Date(session.created * 1000).toISOString() : new Date().toISOString(),
|
||||
};
|
||||
|
||||
const res = await fetch(`${CRM_BASE_URL}/public/founder-access/fulfill`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Founder-Secret": secret },
|
||||
body: JSON.stringify(payload),
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => "");
|
||||
throw new Error(`be-crm fulfil failed: ${res.status} ${detail.slice(0, 200)}`);
|
||||
}
|
||||
console.log("[webhook] purchase fulfilled", { email, session: session.id });
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const secret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
if (!secret) {
|
||||
console.error("[webhook] STRIPE_WEBHOOK_SECRET is not set");
|
||||
return new Response("Webhook secret not configured", { status: 500 }); // 500 -> Stripe retries
|
||||
}
|
||||
|
||||
const raw = Buffer.from(await request.arrayBuffer());
|
||||
let event: { type: string; data: { object: StripeSession } };
|
||||
try {
|
||||
event = verifyStripeSignature(raw, request.headers.get("stripe-signature"), secret);
|
||||
} catch (err) {
|
||||
console.error("[webhook] signature verification failed:", (err as Error).message);
|
||||
return new Response(`Webhook Error: ${(err as Error).message}`, { status: 400 }); // 400 -> don't retry
|
||||
}
|
||||
|
||||
try {
|
||||
if (event.type === "checkout.session.completed" || event.type === "checkout.session.async_payment_succeeded") {
|
||||
await fulfil(event.data.object);
|
||||
} else {
|
||||
console.log("[webhook] ignoring event type", event.type);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[webhook] handler failed", { type: event.type, error: String((err as Error)?.message) });
|
||||
return new Response("Handler error", { status: 500 }); // 500 -> Stripe retries (metadata replays the lead)
|
||||
}
|
||||
|
||||
return Response.json({ received: true });
|
||||
}
|
||||
@@ -3,15 +3,20 @@
|
||||
// ============================================================
|
||||
// Profile — header card + 6 tabs:
|
||||
// Personal Info · KYC · Security · Notifications · Privacy · Devices
|
||||
//
|
||||
// Personal / KYC / Notifications / Privacy are served by the be-crm profile
|
||||
// module (via useProfileData → live data door, or the local mock when the Shell
|
||||
// isn't configured). Security & Devices are Shell-owned and stay on the mock.
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
user, kycDocTypes, kyc as kycSeed, contactPrefs, contactDestinations,
|
||||
contactTimezones, contactTimes, loginActivity, consentGroups,
|
||||
passwordStrength, maskEmail, maskPhone,
|
||||
type KycSlot, type NotifCategory, type ConsentGroup,
|
||||
user, loginActivity, passwordStrength,
|
||||
} from "./account-data";
|
||||
import {
|
||||
useProfileData,
|
||||
type ProfileData, type UiAccount, type UiKycSlot, type ContactKind, type KycKey, type NotifChannel, type UploadFile,
|
||||
} from "@/lib/profile-api";
|
||||
import {
|
||||
Avatar, Btn, Field, Icon, Modal, OtpField, PageHead, Pill, SegTabs,
|
||||
StatusDot, Toggle, useToast,
|
||||
@@ -26,7 +31,27 @@ const TABS = [
|
||||
{ value: "devices", label: "Devices", icon: "devices" },
|
||||
];
|
||||
|
||||
/** Read a picked File into the base64 shape a data-door command carries. */
|
||||
function readFileAsUpload(file: File): Promise<UploadFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = String(reader.result).split(",")[1] ?? "";
|
||||
resolve({ fileName: file.name, contentType: file.type || "application/octet-stream", base64 });
|
||||
};
|
||||
reader.onerror = () => reject(reader.error ?? new Error("could not read file"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/** Turn a thrown data-door error into a short, readable toast message. */
|
||||
function errText(e: unknown): string {
|
||||
const m = e instanceof Error ? e.message : String(e);
|
||||
return m.replace(/^\d+\s*[-:]\s*/, "").slice(0, 160) || "Something went wrong.";
|
||||
}
|
||||
|
||||
export function Profile() {
|
||||
const p = useProfileData();
|
||||
const [tab, setTab] = useState("personal");
|
||||
|
||||
return (
|
||||
@@ -36,19 +61,26 @@ export function Profile() {
|
||||
title="Profile"
|
||||
subtitle="Manage your identity, verification, security and preferences."
|
||||
icon="user"
|
||||
actions={<Pill tone="green"><StatusDot status="online" /> Account active</Pill>}
|
||||
actions={<Pill tone={p.account.status === "active" ? "green" : "red"}><StatusDot status={p.account.status === "active" ? "online" : "offline"} /> Account {p.account.status}</Pill>}
|
||||
/>
|
||||
|
||||
<ProfileHeaderCard />
|
||||
{p.error && (
|
||||
<div className="callout tone-red" style={{ marginBottom: 16 }}>
|
||||
<Icon name="alert" size={18} />
|
||||
<div><strong>Couldn't load your profile.</strong><span>{p.error}</span></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProfileHeaderCard account={p.account} />
|
||||
|
||||
<SegTabs tabs={TABS} value={tab} onChange={setTab} />
|
||||
|
||||
<div className="view-body">
|
||||
{tab === "personal" && <PersonalInfoTab />}
|
||||
{tab === "kyc" && <KycTab />}
|
||||
{tab === "personal" && <PersonalInfoTab p={p} />}
|
||||
{tab === "kyc" && <KycTab p={p} />}
|
||||
{tab === "security" && <SecurityTab />}
|
||||
{tab === "notifications" && <NotificationsTab />}
|
||||
{tab === "privacy" && <PrivacyTab />}
|
||||
{tab === "notifications" && <NotificationsTab p={p} />}
|
||||
{tab === "privacy" && <PrivacyTab p={p} />}
|
||||
{tab === "devices" && <DevicesTab />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,35 +91,36 @@ export function Profile() {
|
||||
/* Header card */
|
||||
/* ============================================================ */
|
||||
|
||||
function ProfileHeaderCard() {
|
||||
function ProfileHeaderCard({ account }: { account: UiAccount }) {
|
||||
const { push } = useToast();
|
||||
const locale = [account.sector, account.pocket].filter(Boolean).join(" · ") || "Location on file";
|
||||
return (
|
||||
<div className="card profile-hero">
|
||||
<div className="profile-hero-bg" />
|
||||
<div className="profile-hero-main">
|
||||
<div className="profile-hero-avatar">
|
||||
<Avatar initials={user.initials} gradient={user.avatarGradient} size={86} square />
|
||||
<Avatar initials={account.initials} gradient={account.avatarGradient} size={86} square />
|
||||
<button className="profile-hero-cam" aria-label="Change photo" onClick={() => push({ tone: "info", title: "Photo upload", desc: "Choose a JPG or PNG up to 5 MB." })}>
|
||||
<Icon name="camera" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="profile-hero-id">
|
||||
<div className="profile-hero-name">
|
||||
{user.name}
|
||||
{account.name}
|
||||
<Icon name="check-circle" size={18} className="verified-badge" />
|
||||
</div>
|
||||
<div className="profile-hero-role">{user.role} · Member since {user.memberSince}</div>
|
||||
<div className="profile-hero-role">{account.role} · Member since {account.memberSince}</div>
|
||||
<div className="profile-hero-chips">
|
||||
<span className="hero-chip"><Icon name="pin" size={13} /> {user.sector} · {user.pocket}</span>
|
||||
<span className="hero-chip"><Icon name="owners" size={13} /> {user.category}</span>
|
||||
<span className="hero-chip accent"><Icon name="check" size={13} /> House {user.plot}</span>
|
||||
<span className="hero-chip"><Icon name="pin" size={13} /> {locale}</span>
|
||||
{account.category && <span className="hero-chip"><Icon name="owners" size={13} /> {account.category}</span>}
|
||||
{account.plot && <span className="hero-chip accent"><Icon name="check" size={13} /> House {account.plot}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-hero-stats">
|
||||
<HeroStat value={user.category} label="Category" />
|
||||
<HeroStat value={user.size} label="House size" />
|
||||
<HeroStat value="33%" label="KYC complete" accent="var(--orange)" />
|
||||
<HeroStat value={account.category ?? "—"} label="Category" />
|
||||
<HeroStat value={account.size ?? "—"} label="House size" />
|
||||
<HeroStat value={`${account.kycCompletionPct}%`} label="KYC complete" accent="var(--orange)" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -105,31 +138,30 @@ function HeroStat({ value, label, accent }: { value: string; label: string; acce
|
||||
/* Tab: Personal Info — verify-to-reveal + change email/mobile */
|
||||
/* ============================================================ */
|
||||
|
||||
type ContactKind = "email" | "mobile" | "whatsapp";
|
||||
|
||||
function PersonalInfoTab() {
|
||||
function PersonalInfoTab({ p }: { p: ProfileData }) {
|
||||
const { push } = useToast();
|
||||
const [verified, setVerified] = useState(false);
|
||||
const [plot, setPlot] = useState("");
|
||||
const { account } = p;
|
||||
const verified = account.revealed;
|
||||
const [house, setHouse] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [change, setChange] = useState<null | ContactKind>(null);
|
||||
|
||||
// editable contact values (live-updating after OTP confirmation)
|
||||
const [contacts, setContacts] = useState({ email: user.email.full, mobile: user.mobile.full, whatsapp: user.whatsapp.full });
|
||||
|
||||
function verify() {
|
||||
if (plot.trim() === user.plot) { setVerified(true); setErr(""); push({ tone: "success", title: "Verified", desc: "Your full details are now visible and editable." }); }
|
||||
else setErr(`That doesn't match the house number on file (hint: it's ${user.plot}).`);
|
||||
async function verify() {
|
||||
if (!house.trim()) return;
|
||||
setBusy(true); setErr("");
|
||||
try {
|
||||
const ok = await p.reveal(house.trim());
|
||||
if (ok) push({ tone: "success", title: "Verified", desc: "Your full details are now visible and editable." });
|
||||
else setErr("That doesn't match the house number on file.");
|
||||
} catch (e) { setErr(errText(e)); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
function mask(kind: ContactKind, full: string) {
|
||||
return kind === "email" ? maskEmail(full) : maskPhone(full);
|
||||
}
|
||||
|
||||
const contactRows: { key: ContactKind; label: string; full: string; masked: string }[] = [
|
||||
{ key: "email", label: "Email", full: contacts.email, masked: mask("email", contacts.email) },
|
||||
{ key: "mobile", label: "Mobile", full: contacts.mobile, masked: mask("mobile", contacts.mobile) },
|
||||
{ key: "whatsapp", label: "WhatsApp", full: contacts.whatsapp, masked: mask("whatsapp", contacts.whatsapp) },
|
||||
const contactRows: { key: ContactKind; label: string; value: string }[] = [
|
||||
{ key: "email", label: "Email", value: account.email },
|
||||
{ key: "mobile", label: "Mobile", value: account.mobile },
|
||||
{ key: "whatsapp", label: "WhatsApp", value: account.whatsapp },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -150,10 +182,10 @@ function PersonalInfoTab() {
|
||||
<span>Sensitive details are masked. Enter your house number to unmask and edit them.</span>
|
||||
</div>
|
||||
<div className="reveal-gate-form">
|
||||
<input className="ds-input" placeholder="House number" value={plot} maxLength={6}
|
||||
onChange={(e) => { setPlot(e.target.value.replace(/\D/g, "")); setErr(""); }}
|
||||
<input className="ds-input" placeholder="House number" value={house} maxLength={16}
|
||||
onChange={(e) => { setHouse(e.target.value); setErr(""); }}
|
||||
onKeyDown={(e) => e.key === "Enter" && verify()} />
|
||||
<Btn icon="check" onClick={verify}>Reveal</Btn>
|
||||
<Btn icon="check" onClick={verify} disabled={busy || !house.trim()}>{busy ? "Verifying…" : "Reveal"}</Btn>
|
||||
</div>
|
||||
{err && <div className="ds-field-err"><Icon name="alert" size={12} /> {err}</div>}
|
||||
</div>
|
||||
@@ -164,7 +196,7 @@ function PersonalInfoTab() {
|
||||
<div className="kv" key={f.key}>
|
||||
<dt>{f.label}</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>
|
||||
{verified ? f.full : f.masked}
|
||||
{f.value}
|
||||
{verified && (
|
||||
<button className="kv-edit-btn" aria-label={`Edit ${f.label}`} onClick={() => setChange(f.key)}><Icon name="edit" size={13} /></button>
|
||||
)}
|
||||
@@ -188,64 +220,82 @@ function PersonalInfoTab() {
|
||||
: <Pill tone="muted"><Icon name="lock" size={12} /> Protected</Pill>}
|
||||
</div>
|
||||
<dl className="kv-list">
|
||||
<div className="kv"><dt>Full name</dt><dd>{user.name}</dd></div>
|
||||
<div className="kv"><dt>Role</dt><dd>{user.role}</dd></div>
|
||||
<div className="kv"><dt>Member since</dt><dd>{user.memberSince}</dd></div>
|
||||
<div className="kv"><dt>Account status</dt><dd><Pill tone="green"><StatusDot status="online" /> Active</Pill></dd></div>
|
||||
<div className="kv"><dt>Full name</dt><dd>{account.name}</dd></div>
|
||||
<div className="kv"><dt>Role</dt><dd>{account.role}</dd></div>
|
||||
{account.allotmentNo && <div className="kv"><dt>Allotment no.</dt><dd>{account.allotmentNo}</dd></div>}
|
||||
<div className="kv"><dt>Member since</dt><dd>{account.memberSince}</dd></div>
|
||||
<div className="kv"><dt>Account status</dt><dd><Pill tone={account.status === "active" ? "green" : "red"}><StatusDot status={account.status === "active" ? "online" : "offline"} /> {account.status === "active" ? "Active" : "Suspended"}</Pill></dd></div>
|
||||
<div className="kv">
|
||||
<dt>PAN</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{verified ? user.pan.full : user.pan.masked}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{account.pan}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
</div>
|
||||
<div className="kv">
|
||||
<dt>Aadhaar</dt>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{verified ? user.aadhaar.full : user.aadhaar.masked}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
<dd className={verified ? "kv-edit" : "masked"}>{account.aadhaar}{verified && <span className="kv-lock"><Icon name="lock" size={12} /> via KYC</span>}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="muted-note"><Icon name="info" size={13} /> PAN & Aadhaar are managed under the KYC tab and can't be edited here.</p>
|
||||
</div>
|
||||
|
||||
<ChangeContactModal
|
||||
kind={change}
|
||||
current={change ? contacts[change] : ""}
|
||||
onClose={() => setChange(null)}
|
||||
onSave={(kind, value) => setContacts((c) => ({ ...c, [kind]: value }))}
|
||||
/>
|
||||
<ChangeContactModal p={p} kind={change} onClose={() => setChange(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CONTACT_LABEL: Record<ContactKind, string> = { email: "email", mobile: "mobile", whatsapp: "WhatsApp number" };
|
||||
|
||||
// OTP-to-registered-mobile flow for changing a contact field. On confirm the
|
||||
// new value is saved back so the Personal Info card updates live.
|
||||
function ChangeContactModal({ kind, current, onClose, onSave }: {
|
||||
kind: null | ContactKind; current: string; onClose: () => void; onSave: (kind: ContactKind, value: string) => void;
|
||||
}) {
|
||||
// OTP-to-registered-mobile flow for changing a contact field. The one-time code
|
||||
// is always sent to the registered mobile (never the new destination), then the
|
||||
// new value is persisted only after the code verifies.
|
||||
function ChangeContactModal({ p, kind, onClose }: { p: ProfileData; kind: null | ContactKind; onClose: () => void }) {
|
||||
const { push } = useToast();
|
||||
const [step, setStep] = useState<"enter" | "otp">("enter");
|
||||
const [val, setVal] = useState("");
|
||||
const [otp, setOtp] = useState("");
|
||||
const [challengeId, setChallengeId] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
// prefill with the current value whenever a new field is opened
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (kind) { setStep("enter"); setVal(current); setOtp(""); }
|
||||
}, [kind, current]);
|
||||
if (kind) { setStep("enter"); setVal(""); setOtp(""); setChallengeId(""); setBusy(false); }
|
||||
}, [kind]);
|
||||
|
||||
function close() { setStep("enter"); setVal(""); setOtp(""); onClose(); }
|
||||
|
||||
const isEmail = kind === "email";
|
||||
const label = kind ? CONTACT_LABEL[kind] : "";
|
||||
|
||||
async function sendCode() {
|
||||
if (!kind) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const id = await p.requestContactChangeOtp(kind, val.trim());
|
||||
setChallengeId(id); setStep("otp");
|
||||
push({ tone: "info", title: "Code sent", desc: "A one-time code was sent to your registered mobile." });
|
||||
} catch (e) { push({ tone: "error", title: "Couldn't send code", desc: errText(e) }); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
async function confirm() {
|
||||
if (!kind) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await p.changeContact(kind, val.trim(), challengeId, otp);
|
||||
push({ tone: "success", title: `${label[0].toUpperCase()}${label.slice(1)} updated`, desc: "Your change has been confirmed." });
|
||||
close();
|
||||
} catch (e) { push({ tone: "error", title: "Couldn't confirm", desc: errText(e) }); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={kind != null} onClose={close}
|
||||
icon={isEmail ? "mail" : kind === "whatsapp" ? "chat" : "phone"}
|
||||
title={`Change ${label}`}
|
||||
subtitle={step === "enter" ? "We'll send a code to your registered mobile to confirm." : `Enter the 6-digit code sent to ${user.mobile.masked}`}
|
||||
subtitle={step === "enter" ? "We'll send a code to your registered mobile to confirm." : "Enter the 6-digit code sent to your registered mobile."}
|
||||
footer={
|
||||
step === "enter"
|
||||
? <><Btn variant="ghost" onClick={close}>Cancel</Btn><Btn icon="send" disabled={!val.trim() || val.trim() === current} onClick={() => { setStep("otp"); push({ tone: "info", title: "Code sent", desc: `OTP sent to your registered mobile ${user.mobile.masked}.` }); }}>Send code</Btn></>
|
||||
: <><Btn variant="ghost" onClick={() => setStep("enter")}>Back</Btn><Btn icon="check" disabled={otp.length < 6} onClick={() => { if (kind) onSave(kind, val.trim()); push({ tone: "success", title: `${label[0].toUpperCase()}${label.slice(1)} updated`, desc: "Your change has been confirmed." }); close(); }}>Confirm</Btn></>
|
||||
? <><Btn variant="ghost" onClick={close}>Cancel</Btn><Btn icon="send" disabled={busy || !val.trim()} onClick={sendCode}>{busy ? "Sending…" : "Send code"}</Btn></>
|
||||
: <><Btn variant="ghost" onClick={() => setStep("enter")}>Back</Btn><Btn icon="check" disabled={busy || otp.length < 6} onClick={confirm}>{busy ? "Confirming…" : "Confirm"}</Btn></>
|
||||
}
|
||||
>
|
||||
{step === "enter" ? (
|
||||
@@ -255,7 +305,7 @@ function ChangeContactModal({ kind, current, onClose, onSave }: {
|
||||
) : (
|
||||
<div className="otp-wrap">
|
||||
<OtpField value={otp} onChange={setOtp} />
|
||||
<button className="link-inline" onClick={() => push({ tone: "info", title: "Code resent" })}>Resend code</button>
|
||||
<button className="link-inline" onClick={sendCode} disabled={busy}>Resend code</button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
@@ -266,54 +316,52 @@ function ChangeContactModal({ kind, current, onClose, onSave }: {
|
||||
/* Tab: KYC — inline upload, ID≠address, photo mandatory, % */
|
||||
/* ============================================================ */
|
||||
|
||||
function KycTab() {
|
||||
function KycTab({ p }: { p: ProfileData }) {
|
||||
const { push } = useToast();
|
||||
const [slots, setSlots] = useState<KycSlot[]>(kycSeed.slots);
|
||||
const { slots, docTypes, completionPct } = p.kyc;
|
||||
// Doc type chosen for a slot but not yet uploaded (be-crm only persists it on upload).
|
||||
const [pending, setPending] = useState<Partial<Record<KycKey, string>>>({});
|
||||
|
||||
const idDoc = slots.find((s) => s.key === "id")?.docId ?? null;
|
||||
const addrDoc = slots.find((s) => s.key === "address")?.docId ?? null;
|
||||
const docIdFor = (key: KycKey) => pending[key] ?? slots.find((s) => s.key === key)?.docId ?? null;
|
||||
const idDoc = docIdFor("id");
|
||||
const addrDoc = docIdFor("address");
|
||||
|
||||
const completion = useMemo(() => {
|
||||
const done = slots.filter((s) => s.status === "verified" || s.status === "uploaded").length;
|
||||
return Math.round((done / slots.length) * 100);
|
||||
}, [slots]);
|
||||
|
||||
function options(key: KycSlot["key"]) {
|
||||
function options(key: KycKey) {
|
||||
const want = key === "id" ? ["id", "any"] : key === "address" ? ["address", "any"] : [];
|
||||
return kycDocTypes.filter((d) => want.includes(d.use));
|
||||
return docTypes.filter((d) => want.includes(d.use));
|
||||
}
|
||||
|
||||
function pickDoc(key: KycSlot["key"], docId: string) {
|
||||
// enforce two-different-documents rule between ID and address
|
||||
function pickDoc(key: KycKey, docId: string) {
|
||||
if (key === "id" && docId === addrDoc) { push({ tone: "error", title: "Pick a different document", desc: "Identity and address proofs must be two different documents." }); return; }
|
||||
if (key === "address" && docId === idDoc) { push({ tone: "error", title: "Pick a different document", desc: "Address and identity proofs must be two different documents." }); return; }
|
||||
setSlots((s) => s.map((x) => x.key === key ? { ...x, docId } : x));
|
||||
setPending((s) => ({ ...s, [key]: docId }));
|
||||
}
|
||||
async function upload(key: KycKey, file: File) {
|
||||
const docId = key === "photo" ? null : docIdFor(key);
|
||||
if (key !== "photo" && !docId) { push({ tone: "error", title: "Select a document type first" }); return; }
|
||||
try {
|
||||
await p.uploadKyc(key, docId, await readFileAsUpload(file));
|
||||
setPending((s) => { const n = { ...s }; delete n[key]; return n; });
|
||||
push({ tone: "success", title: "Uploaded", desc: "Your document is queued for review." });
|
||||
} catch (e) { push({ tone: "error", title: "Upload failed", desc: errText(e) }); }
|
||||
}
|
||||
async function remove(key: KycKey) {
|
||||
try { await p.removeKyc(key); push({ tone: "info", title: "Removed" }); }
|
||||
catch (e) { push({ tone: "error", title: "Couldn't remove", desc: errText(e) }); }
|
||||
}
|
||||
|
||||
function upload(key: KycSlot["key"]) {
|
||||
const slot = slots.find((s) => s.key === key)!;
|
||||
if (key !== "photo" && !slot.docId) { push({ tone: "error", title: "Select a document type first" }); return; }
|
||||
const name = key === "photo" ? "selfie.jpg" : `${slot.docId}_upload.pdf`;
|
||||
setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: name, status: "uploaded", note: "Under review" } : x));
|
||||
push({ tone: "success", title: "Uploaded", desc: "Your document is queued for review." });
|
||||
}
|
||||
|
||||
function remove(key: KycSlot["key"]) {
|
||||
setSlots((s) => s.map((x) => x.key === key ? { ...x, fileName: null, docId: key === "photo" ? null : x.docId, status: "missing", note: undefined } : x));
|
||||
}
|
||||
|
||||
const photoDone = slots.find((s) => s.key === "photo")!.status !== "missing";
|
||||
const photoDone = (slots.find((s) => s.key === "photo")?.status ?? "missing") !== "missing";
|
||||
const docLabel = (id: string | null) => docTypes.find((d) => d.id === id)?.label ?? "Not chosen";
|
||||
|
||||
return (
|
||||
<div className="grid-side">
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Verification documents</h3>
|
||||
<Pill tone="orange">{completion}% complete</Pill>
|
||||
<Pill tone="orange">{completionPct}% complete</Pill>
|
||||
</div>
|
||||
|
||||
<div className="kyc-progress">
|
||||
<div className="kyc-progress-bar"><span style={{ width: `${completion}%` }} /></div>
|
||||
<div className="kyc-progress-bar"><span style={{ width: `${completionPct}%` }} /></div>
|
||||
<div className="kyc-progress-legend">
|
||||
{slots.map((s) => (
|
||||
<span key={s.key} className={`kyc-leg ${s.status}`}>
|
||||
@@ -327,7 +375,8 @@ function KycTab() {
|
||||
|
||||
<div className="kyc-slots">
|
||||
{slots.map((slot) => (
|
||||
<KycSlotRow key={slot.key} slot={slot} options={options(slot.key)} onPick={(d) => pickDoc(slot.key, d)} onUpload={() => upload(slot.key)} onRemove={() => remove(slot.key)} />
|
||||
<KycSlotRow key={slot.key} slot={slot} docId={docIdFor(slot.key)} docTypes={docTypes} options={options(slot.key)}
|
||||
onPick={(d) => pickDoc(slot.key, d)} onUpload={(f) => upload(slot.key, f)} onRemove={() => remove(slot.key)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,19 +392,22 @@ function KycTab() {
|
||||
</ul>
|
||||
<div className="kyc-doc-pairs">
|
||||
<div className="muted-note"><Icon name="info" size={13} /> Current selection</div>
|
||||
<div className="pair-row"><span>Identity</span><Pill tone="blue">{kycDocTypes.find((d) => d.id === idDoc)?.label ?? "Not chosen"}</Pill></div>
|
||||
<div className="pair-row"><span>Address</span><Pill tone="purple">{kycDocTypes.find((d) => d.id === addrDoc)?.label ?? "Not chosen"}</Pill></div>
|
||||
<div className="pair-row"><span>Identity</span><Pill tone="blue">{docLabel(idDoc)}</Pill></div>
|
||||
<div className="pair-row"><span>Address</span><Pill tone="purple">{docLabel(addrDoc)}</Pill></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KycSlotRow({ slot, options, onPick, onUpload, onRemove }: {
|
||||
slot: KycSlot; options: typeof kycDocTypes; onPick: (d: string) => void; onUpload: () => void; onRemove: () => void;
|
||||
function KycSlotRow({ slot, docId, docTypes, options, onPick, onUpload, onRemove }: {
|
||||
slot: UiKycSlot; docId: string | null; docTypes: { id: string; label: string; hint: string; formats: string; maxMb: number }[];
|
||||
options: { id: string; label: string }[]; onPick: (d: string) => void; onUpload: (f: File) => void; onRemove: () => void;
|
||||
}) {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const tone = slot.status === "verified" ? "green" : slot.status === "uploaded" ? "blue" : slot.status === "rejected" ? "red" : "muted";
|
||||
const docMeta = kycDocTypes.find((d) => d.id === slot.docId);
|
||||
const docMeta = docTypes.find((d) => d.id === docId);
|
||||
const accept = slot.key === "photo" ? "image/*" : "application/pdf,image/jpeg,image/png";
|
||||
return (
|
||||
<div className={`kyc-slot status-${slot.status}`}>
|
||||
<div className="kyc-slot-ic"><Icon name={slot.key === "photo" ? "camera" : slot.key === "id" ? "user" : "pin"} size={18} /></div>
|
||||
@@ -366,23 +418,27 @@ function KycSlotRow({ slot, options, onPick, onUpload, onRemove }: {
|
||||
</div>
|
||||
|
||||
{slot.key !== "photo" && (
|
||||
<select className="ds-select" value={slot.docId ?? ""} onChange={(e) => onPick(e.target.value)}>
|
||||
<select className="ds-select" value={docId ?? ""} onChange={(e) => onPick(e.target.value)}>
|
||||
<option value="" disabled>Select document type…</option>
|
||||
{options.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||
</select>
|
||||
)}
|
||||
|
||||
{docMeta && <div className="kyc-slot-hint">{docMeta.hint} · {docMeta.formats} · ≤ {docMeta.maxMb} MB</div>}
|
||||
{docMeta && <div className="kyc-slot-hint">{docMeta.hint ? `${docMeta.hint} · ` : ""}{docMeta.formats} · ≤ {docMeta.maxMb} MB</div>}
|
||||
{slot.status === "rejected" && slot.note && <div className="ds-field-err"><Icon name="alert" size={12} /> {slot.note}</div>}
|
||||
|
||||
<input ref={fileRef} type="file" accept={accept} hidden
|
||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ""; }} />
|
||||
|
||||
{slot.fileName ? (
|
||||
<div className="kyc-file">
|
||||
<Icon name="paperclip" size={14} />
|
||||
<span className="kyc-file-name">{slot.fileName}</span>
|
||||
{slot.note && <span className="kyc-file-note">{slot.note}</span>}
|
||||
{slot.note && slot.status !== "rejected" && <span className="kyc-file-note">{slot.note}</span>}
|
||||
<button className="ds-iconbtn sm" aria-label="Remove" onClick={onRemove}><Icon name="trash" size={14} /></button>
|
||||
</div>
|
||||
) : (
|
||||
<button className="kyc-drop" onClick={onUpload}>
|
||||
<button className="kyc-drop" onClick={() => fileRef.current?.click()}>
|
||||
<Icon name="upload" size={16} /> Click to upload {slot.key === "photo" ? "a photo" : "document"}
|
||||
</button>
|
||||
)}
|
||||
@@ -392,7 +448,7 @@ function KycSlotRow({ slot, options, onPick, onUpload, onRemove }: {
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* Tab: Security */
|
||||
/* Tab: Security (Shell-owned — local mock) */
|
||||
/* ============================================================ */
|
||||
|
||||
function SecurityTab() {
|
||||
@@ -478,24 +534,29 @@ function SettingRow({ icon, title, desc, children }: { icon: string; title: stri
|
||||
/* Tab: Notifications */
|
||||
/* ============================================================ */
|
||||
|
||||
function NotificationsTab() {
|
||||
function NotificationsTab({ p }: { p: ProfileData }) {
|
||||
const { push } = useToast();
|
||||
const [prefs, setPrefs] = useState<NotifCategory[]>(contactPrefs);
|
||||
const [tz, setTz] = useState(contactTimezones[0]);
|
||||
const [times, setTimes] = useState(contactTimes);
|
||||
const channels: { key: keyof NotifCategory; label: string; icon: string }[] = [
|
||||
const { prefs, timezone, timezoneOptions, windows, destinations } = p.notifications;
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [verifyId, setVerifyId] = useState<string | null>(null);
|
||||
const channels: { key: NotifChannel; label: string; icon: string }[] = [
|
||||
{ key: "email", label: "Email", icon: "mail" },
|
||||
{ key: "sms", label: "SMS", icon: "phone" },
|
||||
{ key: "whatsapp", label: "WhatsApp", icon: "chat" },
|
||||
{ key: "push", label: "Push", icon: "bell" },
|
||||
];
|
||||
|
||||
function toggle(catId: string, ch: keyof NotifCategory) {
|
||||
setPrefs((p) => p.map((c) => {
|
||||
if (c.id !== catId) return c;
|
||||
if (c.locked) { push({ tone: "info", title: "Required notifications", desc: "Security alerts can't be turned off." }); return c; }
|
||||
return { ...c, [ch]: !c[ch] } as NotifCategory;
|
||||
}));
|
||||
async function toggle(cat: (typeof prefs)[number], ch: NotifChannel) {
|
||||
if (cat.locked) { push({ tone: "info", title: "Required notifications", desc: "Security alerts can't be turned off." }); return; }
|
||||
try { await p.setPreference(cat.id, ch, !cat[ch]); }
|
||||
catch (e) { push({ tone: "error", title: "Couldn't save", desc: errText(e) }); }
|
||||
}
|
||||
async function toggleWindow(w: (typeof windows)[number]) {
|
||||
try { await p.setContactWindow(w.label, { startTime: w.startTime, endTime: w.endTime, enabled: !w.enabled }); }
|
||||
catch (e) { push({ tone: "error", title: "Couldn't save", desc: errText(e) }); }
|
||||
}
|
||||
async function changeTz(tz: string) {
|
||||
try { await p.setTimezone(tz); } catch (e) { push({ tone: "error", title: "Couldn't save", desc: errText(e) }); }
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -515,7 +576,7 @@ function NotificationsTab() {
|
||||
</span>
|
||||
{channels.map((c) => (
|
||||
<span key={c.key} className="notif-ch">
|
||||
<Toggle checked={Boolean(cat[c.key])} disabled={cat.locked} onChange={() => toggle(cat.id, c.key)} label={`${cat.label} ${c.label}`} />
|
||||
<Toggle checked={Boolean(cat[c.key])} disabled={cat.locked} onChange={() => toggle(cat, c.key)} label={`${cat.label} ${c.label}`} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -527,13 +588,13 @@ function NotificationsTab() {
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>Contact-time window</h3></div>
|
||||
<Field label="Timezone">
|
||||
<select className="ds-select" value={tz} onChange={(e) => setTz(e.target.value)}>
|
||||
{contactTimezones.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
<select className="ds-select" value={timezone} onChange={(e) => changeTz(e.target.value)}>
|
||||
{timezoneOptions.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="time-windows">
|
||||
{times.map((t) => (
|
||||
<button key={t.id} className={`time-win ${t.enabled ? "on" : ""}`} onClick={() => setTimes((s) => s.map((x) => x.id === t.id ? { ...x, enabled: !x.enabled } : x))}>
|
||||
{windows.map((t) => (
|
||||
<button key={t.label} className={`time-win ${t.enabled ? "on" : ""}`} onClick={() => toggleWindow(t)}>
|
||||
<Icon name={t.enabled ? "check-circle" : "clock"} size={15} />
|
||||
<span className="tw-label">{t.label}</span>
|
||||
<span className="tw-range">{t.range}</span>
|
||||
@@ -546,76 +607,155 @@ function NotificationsTab() {
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>Destinations</h3></div>
|
||||
<div className="dest-list">
|
||||
{contactDestinations.map((d) => (
|
||||
{destinations.length === 0 && <p className="muted-note"><Icon name="info" size={13} /> No extra destinations yet.</p>}
|
||||
{destinations.map((d) => (
|
||||
<div className="dest-row" key={d.id}>
|
||||
<span className="dest-ic"><Icon name={d.channel === "email" ? "mail" : d.channel === "sms" ? "phone" : "chat"} size={15} /></span>
|
||||
<div className="dest-txt"><div className="dest-label">{d.label}{d.primary && <Pill tone="orange">Primary</Pill>}</div><div className="dest-val">{d.value}</div></div>
|
||||
{d.verified ? <Pill tone="green"><Icon name="check" size={12} /> Verified</Pill> : <Btn variant="outline" size="sm" onClick={() => push({ tone: "info", title: "Verification sent" })}>Verify</Btn>}
|
||||
{d.verified ? <Pill tone="green"><Icon name="check" size={12} /> Verified</Pill> : <Btn variant="outline" size="sm" onClick={() => setVerifyId(d.id)}>Verify</Btn>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Btn variant="ghost" icon="plus" onClick={() => push({ tone: "info", title: "Add destination" })}>Add destination</Btn>
|
||||
<Btn variant="ghost" icon="plus" onClick={() => setAddOpen(true)}>Add destination</Btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddDestinationModal p={p} open={addOpen} onClose={() => setAddOpen(false)} />
|
||||
<VerifyDestinationModal p={p} id={verifyId} onClose={() => setVerifyId(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddDestinationModal({ p, open, onClose }: { p: ProfileData; open: boolean; onClose: () => void }) {
|
||||
const { push } = useToast();
|
||||
const [channel, setChannel] = useState<"email" | "sms" | "whatsapp">("email");
|
||||
const [label, setLabel] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (open) { setChannel("email"); setLabel(""); setValue(""); setBusy(false); }
|
||||
}, [open]);
|
||||
|
||||
async function save() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await p.addDestination(channel, label.trim() || channel, value.trim());
|
||||
push({ tone: "success", title: "Destination added", desc: "Verify it to start receiving messages there." });
|
||||
onClose();
|
||||
} catch (e) { push({ tone: "error", title: "Couldn't add", desc: errText(e) }); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
const isEmail = channel === "email";
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} icon="plus" title="Add a destination"
|
||||
subtitle="Add another address or number, then verify it with a one-time code."
|
||||
footer={<><Btn variant="ghost" onClick={onClose}>Cancel</Btn><Btn icon="check" disabled={busy || !value.trim()} onClick={save}>{busy ? "Adding…" : "Add destination"}</Btn></>}
|
||||
>
|
||||
<Field label="Channel">
|
||||
<select className="ds-select" value={channel} onChange={(e) => setChannel(e.target.value as "email" | "sms" | "whatsapp")}>
|
||||
<option value="email">Email</option>
|
||||
<option value="sms">SMS</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Label" hint="A name to recognise this destination.">
|
||||
<input className="ds-input" placeholder={isEmail ? "Work email" : "Alternate mobile"} value={label} onChange={(e) => setLabel(e.target.value)} />
|
||||
</Field>
|
||||
<Field label={isEmail ? "Email address" : "Phone number"}>
|
||||
<input className="ds-input" type={isEmail ? "email" : "tel"} placeholder={isEmail ? "you@example.com" : "+91 90000 00000"} value={value} onChange={(e) => setValue(e.target.value)} />
|
||||
</Field>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function VerifyDestinationModal({ p, id, onClose }: { p: ProfileData; id: string | null; onClose: () => void }) {
|
||||
const { push } = useToast();
|
||||
const [otp, setOtp] = useState("");
|
||||
const [challengeId, setChallengeId] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [sent, setSent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
let cancelled = false;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setOtp(""); setChallengeId(""); setSent(false); setBusy(true);
|
||||
p.requestDestinationOtp(id)
|
||||
.then((cid) => { if (!cancelled) { setChallengeId(cid); setSent(true); } })
|
||||
.catch((e) => { if (!cancelled) push({ tone: "error", title: "Couldn't send code", desc: errText(e) }); })
|
||||
.finally(() => { if (!cancelled) setBusy(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [id, p, push]);
|
||||
|
||||
async function confirm() {
|
||||
if (!id) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await p.verifyDestination(id, challengeId, otp);
|
||||
push({ tone: "success", title: "Destination verified" });
|
||||
onClose();
|
||||
} catch (e) { push({ tone: "error", title: "Couldn't verify", desc: errText(e) }); }
|
||||
finally { setBusy(false); }
|
||||
}
|
||||
return (
|
||||
<Modal open={id != null} onClose={onClose} icon="shield-check" title="Verify destination"
|
||||
subtitle="Enter the 6-digit code sent to your registered mobile."
|
||||
footer={<><Btn variant="ghost" onClick={onClose}>Cancel</Btn><Btn icon="check" disabled={busy || otp.length < 6} onClick={confirm}>{busy ? "Verifying…" : "Verify"}</Btn></>}
|
||||
>
|
||||
<div className="otp-wrap">
|
||||
<OtpField value={otp} onChange={setOtp} />
|
||||
<span className="muted-note">{sent ? "A code was sent to your registered mobile." : "Sending a code…"}</span>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* Tab: Privacy & Consent */
|
||||
/* ============================================================ */
|
||||
|
||||
function PrivacyTab() {
|
||||
function PrivacyTab({ p }: { p: ProfileData }) {
|
||||
const { push } = useToast();
|
||||
const [groups, setGroups] = useState<ConsentGroup[]>(consentGroups);
|
||||
|
||||
function toggle(gid: string, iid: string) {
|
||||
setGroups((gs) => gs.map((g) => g.id !== gid ? g : {
|
||||
...g,
|
||||
items: g.items.map((it) => {
|
||||
if (it.id !== iid) return it;
|
||||
if (it.locked) { push({ tone: "info", title: "Required consent", desc: "Statutory consents can't be withdrawn while active." }); return it; }
|
||||
return { ...it, granted: !it.granted };
|
||||
}),
|
||||
}));
|
||||
async function toggle(gid: string, iid: string, locked: boolean, granted: boolean) {
|
||||
if (locked) { push({ tone: "info", title: "Required consent", desc: "Statutory consents can't be withdrawn while active." }); return; }
|
||||
try { await p.setConsent(gid, iid, !granted); }
|
||||
catch (e) { push({ tone: "error", title: "Couldn't save", desc: errText(e) }); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="consent-cols">
|
||||
{groups.map((g) => (
|
||||
<div className="card" key={g.id}>
|
||||
<div className="card-head">
|
||||
<h3>{g.title}</h3>
|
||||
{g.id === "marketing" && <Pill tone="muted">Optional</Pill>}
|
||||
{g.id === "essential" && <Pill tone="red"><Icon name="lock" size={12} /> Required</Pill>}
|
||||
</div>
|
||||
<div className="consent-list">
|
||||
{g.items.map((it) => (
|
||||
<div className={`consent-item ${it.locked ? "locked" : ""}`} key={it.id}>
|
||||
<div className="consent-txt">
|
||||
<div className="consent-label">{it.label}{it.locked && <Icon name="lock" size={12} className="lock-ic" />}</div>
|
||||
<div className="consent-desc">{it.desc}</div>
|
||||
{p.consent.map((g) => {
|
||||
const required = g.items.some((it) => it.locked);
|
||||
return (
|
||||
<div className="card" key={g.id}>
|
||||
<div className="card-head">
|
||||
<h3>{g.title}</h3>
|
||||
{required
|
||||
? <Pill tone="red"><Icon name="lock" size={12} /> Required</Pill>
|
||||
: <Pill tone="muted">Optional</Pill>}
|
||||
</div>
|
||||
<div className="consent-list">
|
||||
{g.items.map((it) => (
|
||||
<div className={`consent-item ${it.locked ? "locked" : ""}`} key={it.id}>
|
||||
<div className="consent-txt">
|
||||
<div className="consent-label">{it.label}{it.locked && <Icon name="lock" size={12} className="lock-ic" />}</div>
|
||||
{it.desc && <div className="consent-desc">{it.desc}</div>}
|
||||
</div>
|
||||
<Toggle checked={it.granted} disabled={it.locked} onChange={() => toggle(g.id, it.id, it.locked, it.granted)} label={it.label} />
|
||||
</div>
|
||||
<Toggle checked={it.granted} disabled={it.locked} onChange={() => toggle(g.id, it.id)} label={it.label} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{g.id === "marketing" && groups.find((x) => x.id === "marketing")!.items.find((i) => i.id === "promos")!.granted && (
|
||||
<div className="consent-sub">
|
||||
<div className="muted-note">Promotional sub-preferences</div>
|
||||
{["Product offers", "Surveys & feedback", "Partner news"].map((s, i) => (
|
||||
<label className="consent-sub-row" key={s}><span>{s}</span><Toggle checked={i === 0} onChange={() => {}} label={s} /></label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* Tab: Devices */
|
||||
/* Tab: Devices (Shell-owned — local mock) */
|
||||
/* ============================================================ */
|
||||
|
||||
function DevicesTab() {
|
||||
|
||||
@@ -55,10 +55,43 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
|
||||
const [phoneVerified, setPhoneVerified] = useState(false);
|
||||
// When arriving from a team invite, the email is fixed to the invited address.
|
||||
const [invitedEmail, setInvitedEmail] = useState("");
|
||||
// Paid founder-access token (from the ?ft= link / the /thanks redirect / welcome email).
|
||||
const [founderToken, setFounderToken] = useState("");
|
||||
// Registration is founder-access-only right now: a paid founder (ft token) or a team
|
||||
// invitee may register; open sign-ups are blocked. "used" = the founder link is spent.
|
||||
const [gate, setGate] = useState<"loading" | "allowed" | "blocked" | "used">(onboard ? "allowed" : "loading");
|
||||
|
||||
useEffect(() => {
|
||||
if (onboard) return;
|
||||
try { const e = sessionStorage.getItem("invite_email"); if (e) { setEmail(e); setInvitedEmail(e); } } catch { /* ignore */ }
|
||||
const ft = new URLSearchParams(window.location.search).get("ft") ?? "";
|
||||
let inviteEmail = "";
|
||||
try { inviteEmail = sessionStorage.getItem("invite_email") ?? ""; } catch { /* ignore */ }
|
||||
|
||||
if (ft) {
|
||||
// Validate + prefill from the paid founder record before letting them register.
|
||||
fetch(`/api/founder/lookup?token=${encodeURIComponent(ft)}`, { cache: "no-store" })
|
||||
.then((r) => r.json())
|
||||
.then((d: { ok?: boolean; status?: string; email?: string; firstName?: string; lastName?: string; phone?: string }) => {
|
||||
if (d?.ok && d.status === "valid") {
|
||||
setFounderToken(ft);
|
||||
if (d.email) { setEmail(d.email); setInvitedEmail(d.email); } // reuse the email-lock UI
|
||||
if (d.firstName) setFirst(d.firstName);
|
||||
if (d.lastName) setLast(d.lastName);
|
||||
if (d.phone) setPhone(String(d.phone).replace(/\D/g, "").slice(-10));
|
||||
setGate("allowed");
|
||||
} else if (d?.status === "used") {
|
||||
setGate("used");
|
||||
} else {
|
||||
setGate("blocked");
|
||||
}
|
||||
})
|
||||
.catch(() => setGate("blocked"));
|
||||
} else if (inviteEmail) {
|
||||
setEmail(inviteEmail); setInvitedEmail(inviteEmail);
|
||||
setGate("allowed"); // invited team member
|
||||
} else {
|
||||
setGate("blocked"); // open registration is disabled
|
||||
}
|
||||
}, [onboard]);
|
||||
|
||||
// Onboarding: prefill the verified email — from the session-storage hint the login
|
||||
@@ -132,10 +165,13 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
|
||||
mailingAddress: mailing,
|
||||
mailingSameAsRegistered: mailingSame,
|
||||
consentTerms: termsOk, consentPrivacy: privacyOk,
|
||||
// Paid-founder registration: claims the token + provisions owner in be-crm.
|
||||
...(founderToken ? { founderToken } : {}),
|
||||
});
|
||||
} catch {
|
||||
if (onboard) { setSubmitErr("Couldn't save your profile. Please try again."); return; }
|
||||
// register mode: non-fatal — the auth account exists; the profile can be filled in later.
|
||||
// For onboarding OR a paid founder this step matters (owner provisioning), so
|
||||
// surface it. In plain register mode it's non-fatal — the auth account exists.
|
||||
if (onboard || founderToken) { setSubmitErr("Couldn't finish setting up your account. Please try again."); return; }
|
||||
}
|
||||
|
||||
// 3) If the user arrived from a team invitation link, redeem it now — this
|
||||
@@ -154,6 +190,22 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
// Founder-access gate: only a paid founder or a team invitee may register right now.
|
||||
if (gate === "loading") {
|
||||
return <div className="card anim-fade-up" style={{ textAlign: "center", padding: "44px 0", color: "var(--muted, #94a3b8)" }}>Checking your access…</div>;
|
||||
}
|
||||
if (gate === "used") {
|
||||
return <FounderNotice icon="check" title="This link has already been used"
|
||||
body="Your Founders account is already set up. Please sign in to continue."
|
||||
cta="Go to sign in" onCta={() => router.push("/portal/login")} />;
|
||||
}
|
||||
if (gate === "blocked") {
|
||||
return <FounderNotice icon="lock" title="Founder access required"
|
||||
body="Registration is currently open to Founding Members only. Get founder access to create your account, or sign in if you already have one."
|
||||
cta="Get founder access" onCta={() => { window.location.href = "/"; }}
|
||||
secondaryCta="Sign in" onSecondary={() => router.push("/portal/login")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card anim-fade-up">
|
||||
<Stepper current={step} labels={STEP_LABELS} />
|
||||
@@ -192,6 +244,28 @@ export function RegisterFlow({ mode = "register" }: { mode?: "register" | "onboa
|
||||
);
|
||||
}
|
||||
|
||||
/* ====================== FOUNDER-ACCESS GATE ====================== */
|
||||
function FounderNotice({ icon, title, body, cta, onCta, secondaryCta, onSecondary }: {
|
||||
icon: string; title: string; body: string; cta: string; onCta: () => void;
|
||||
secondaryCta?: string; onSecondary?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="card anim-fade-up" style={{ textAlign: "center" }}>
|
||||
<div style={{ display: "flex", justifyContent: "center", marginBottom: 14 }}>
|
||||
<span className="addr-tile addr-tile-amber" style={{ width: 52, height: 52 }}><Icon name={icon} size={22} /></span>
|
||||
</div>
|
||||
<h1>{title}</h1>
|
||||
<p className="sub" style={{ marginTop: 8 }}>{body}</p>
|
||||
<button className="btn btn-primary" style={{ marginTop: 20 }} onClick={onCta}>{cta} <Icon name="arrowR" size={16} /></button>
|
||||
{secondaryCta && onSecondary && (
|
||||
<p className="foot-note" style={{ marginTop: 14 }}>
|
||||
Already a member? <button className="link" onClick={onSecondary}>{secondaryCta}</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ====================== STEP 0 — ACCOUNT ====================== */
|
||||
function StepAccount(p: {
|
||||
sso: { provider: string; email: string } | null; setSso: (v: { provider: string; email: string } | null) => void;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Shared founder-access lead shape + server-side validation. Ported from the
|
||||
// landing's api/_lead.js: we collect the buyer's details on our own form and only
|
||||
// then hand off to Stripe, so the payment method (incl. Apple Pay express) can
|
||||
// never skip contact capture. The client form validates for UX; this validates
|
||||
// for real. Keep in sync with public/square-checkout.js.
|
||||
|
||||
export const SALES_REP_BUCKETS = ["1", "2-5", "6-10", "11-25", "26-50", "51+"] as const;
|
||||
|
||||
// 1..SELF_SERVE_MAX -> self-serve Stripe checkout. >SELF_SERVE_MAX -> sales.
|
||||
export const MAX_LICENSES = 10;
|
||||
export const SELF_SERVE_MAX = 5;
|
||||
|
||||
export interface Lead {
|
||||
firstName: string; lastName: string; contactName: string;
|
||||
companyName: string; salesReps: string;
|
||||
streetAddress: string; city: string; state: string; zip: string; businessAddress: string;
|
||||
phone: string; email: string; smsConsent: boolean; licenses: number;
|
||||
}
|
||||
|
||||
export type ValidateResult =
|
||||
| { ok: true; lead: Lead }
|
||||
| { ok: false; errors: Record<string, string> };
|
||||
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
firstName: "First name", lastName: "Last name", companyName: "Company name",
|
||||
salesReps: "Number of sales reps", streetAddress: "Street address", city: "City",
|
||||
state: "State", zip: "ZIP code", phone: "Phone number", email: "Email address",
|
||||
smsConsent: "SMS consent",
|
||||
};
|
||||
|
||||
function str(v: unknown): string {
|
||||
return typeof v === "string" ? v.trim() : "";
|
||||
}
|
||||
// Deliberately permissive — a marketing lead form, not an auth system.
|
||||
function isEmail(v: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
|
||||
}
|
||||
function digitCount(v: string): number {
|
||||
return (v.match(/\d/g) || []).length;
|
||||
}
|
||||
|
||||
/** Validate + normalize the payload posted from the checkout form. */
|
||||
export function validateLead(body: Record<string, unknown>): ValidateResult {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
const firstName = str(body.firstName);
|
||||
const lastName = str(body.lastName);
|
||||
const companyName = str(body.companyName);
|
||||
const salesReps = str(body.salesReps);
|
||||
const streetAddress = str(body.streetAddress);
|
||||
const city = str(body.city);
|
||||
const state = str(body.state);
|
||||
const zip = str(body.zip);
|
||||
const phone = str(body.phone);
|
||||
const email = str(body.email).toLowerCase();
|
||||
const smsConsent = body.smsConsent === true || body.smsConsent === "true" || body.smsConsent === "on";
|
||||
|
||||
const contactName = `${firstName} ${lastName}`.trim();
|
||||
const businessAddress = [streetAddress, city, [state, zip].filter(Boolean).join(" ")]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
// Absent -> 1 (older clients / the $1 demo). Present but out of range -> error,
|
||||
// so the buyer sees it rather than being silently clamped to a different charge.
|
||||
let licenses = 1;
|
||||
const raw = body.licenses;
|
||||
if (raw !== undefined && raw !== null && String(raw).trim() !== "") {
|
||||
const n = Number(raw);
|
||||
if (!Number.isInteger(n) || n < 1 || n > MAX_LICENSES) {
|
||||
errors.licenses = `Please choose between 1 and ${MAX_LICENSES} licenses.`;
|
||||
} else {
|
||||
licenses = n;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstName.length < 1) errors.firstName = "Please enter your first name.";
|
||||
if (lastName.length < 1) errors.lastName = "Please enter your last name.";
|
||||
if (companyName.length < 2) errors.companyName = "Please enter your company name.";
|
||||
if (!SALES_REP_BUCKETS.includes(salesReps as (typeof SALES_REP_BUCKETS)[number])) {
|
||||
errors.salesReps = "Please select how many sales reps you have.";
|
||||
}
|
||||
if (streetAddress.length < 3) errors.streetAddress = "Please enter your street address.";
|
||||
if (city.length < 2) errors.city = "Please enter your city.";
|
||||
if (state.length < 2) errors.state = "Please enter your state.";
|
||||
if (!/^\d{5}(-\d{4})?$/.test(zip)) errors.zip = "Please enter a valid ZIP code.";
|
||||
|
||||
const digits = digitCount(phone);
|
||||
if (digits < 10 || digits > 15) errors.phone = "Please enter a valid phone number.";
|
||||
if (!isEmail(email)) errors.email = "Please enter a valid email address.";
|
||||
// SMS consent is required — the founder flow depends on being able to SMS members.
|
||||
if (!smsConsent) errors.smsConsent = "Please agree to receive SMS updates to continue.";
|
||||
|
||||
// Guard against absurd input reaching Stripe metadata (500-char cap) or the CRM.
|
||||
for (const [key, label] of Object.entries(FIELD_LABELS)) {
|
||||
const value = str(body[key]);
|
||||
if (!errors[key] && value.length > 300) errors[key] = `${label} is too long.`;
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length) return { ok: false, errors };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
lead: {
|
||||
firstName, lastName, contactName, companyName, salesReps,
|
||||
streetAddress, city, state, zip, businessAddress,
|
||||
phone, email, smsConsent, licenses,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Stripe metadata (strings only; <=50 keys, values <=500 chars). Stripe is the
|
||||
* system of record for what was captured at purchase, so a failed fulfill can be
|
||||
* replayed from the webhook. */
|
||||
export function leadToStripeMetadata(lead: Lead, extra?: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
first_name: lead.firstName, last_name: lead.lastName, contact_name: lead.contactName,
|
||||
company_name: lead.companyName, sales_reps: lead.salesReps,
|
||||
street_address: lead.streetAddress, city: lead.city, state: lead.state, zip: lead.zip,
|
||||
business_address: lead.businessAddress, phone: lead.phone, email: lead.email,
|
||||
sms_consent: lead.smsConsent ? "true" : "false", num_licenses: String(lead.licenses || 1),
|
||||
...(extra ?? {}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
"use client";
|
||||
|
||||
// Profile data layer. Serves EITHER the local mock (when the Shell isn't configured
|
||||
// — the polished demo keeps working) OR the live be-crm data door, behind one
|
||||
// interface so the Profile screens are mode-agnostic. Mirrors team-api.ts.
|
||||
//
|
||||
// Live contract (be-crm profile module):
|
||||
// query crm.account.read -> AccountReadDTO
|
||||
// cmd crm.account.revealField { houseNumber } -> { revealed }
|
||||
// cmd crm.account.update { …fields } -> AccountReadDTO
|
||||
// cmd crm.account.requestContactChangeOtp { channel, newValue } -> { challengeId }
|
||||
// cmd crm.account.changeContact { channel, newValue, challengeId, otp }
|
||||
// query crm.kyc.list -> KycListDTO
|
||||
// cmd crm.kyc.upload { slot, docTypeId?, file }
|
||||
// cmd crm.kyc.remove { slot }
|
||||
// query crm.notifications.get -> NotificationsGetDTO
|
||||
// cmd crm.notifications.setPreference/setTimezone/setContactWindow
|
||||
// cmd crm.notifications.addDestination/verifyDestination
|
||||
// query crm.consent.list -> ConsentGroupDTO[]
|
||||
// cmd crm.consent.set { groupId, itemId, granted }
|
||||
//
|
||||
// Security (password / 2FA / login alerts) and Devices are Shell-owned and are
|
||||
// intentionally absent here — those tabs stay on the local mock.
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useAppShell, useAuth, useQuery } from "@abe-kap/appshell-sdk/react";
|
||||
import { isShellConfigured } from "./appshell";
|
||||
import {
|
||||
user as seedUser, kyc as seedKyc, kycDocTypes as seedDocTypes,
|
||||
contactPrefs as seedPrefs, contactDestinations as seedDests,
|
||||
contactTimezones as seedTz, contactTimes as seedWindows, consentGroups as seedConsent,
|
||||
} from "@/components/dashboard/account-data";
|
||||
|
||||
/* ---- UI-facing shapes ---------------------------------------------------- */
|
||||
|
||||
export type ContactKind = "email" | "mobile" | "whatsapp";
|
||||
export type KycKey = "photo" | "id" | "address";
|
||||
export type KycStatus = "missing" | "uploaded" | "verified" | "rejected";
|
||||
export type NotifChannel = "email" | "sms" | "whatsapp" | "push";
|
||||
export type DestChannel = "email" | "sms" | "whatsapp";
|
||||
|
||||
export interface UiAccount {
|
||||
principalId: string;
|
||||
name: string; role: string; initials: string; avatarGradient: string;
|
||||
memberSince: string; status: "active" | "suspended";
|
||||
allotmentNo: string | null; plot: string | null; sector: string | null;
|
||||
pocket: string | null; category: string | null; size: string | null;
|
||||
isAllottee: boolean; relationship: string | null; allotteeName: string | null;
|
||||
// Display values — masked unless `revealed`.
|
||||
email: string; mobile: string; whatsapp: string; pan: string; aadhaar: string;
|
||||
revealed: boolean; kycCompletionPct: number;
|
||||
}
|
||||
|
||||
export interface UiKycSlot {
|
||||
key: KycKey; title: string; required: boolean;
|
||||
docId: string | null; fileName: string | null; status: KycStatus; note?: string;
|
||||
}
|
||||
export interface UiDocType { id: string; label: string; use: string; hint: string; formats: string; maxMb: number }
|
||||
export interface UiKyc { slots: UiKycSlot[]; docTypes: UiDocType[]; completionPct: number }
|
||||
|
||||
export interface UiNotifCategory { id: string; label: string; desc: string; locked: boolean; email: boolean; sms: boolean; whatsapp: boolean; push: boolean }
|
||||
export interface UiWindow { id: string; label: string; range: string; startTime: string; endTime: string; enabled: boolean }
|
||||
export interface UiDestination { id: string; channel: DestChannel; label: string; value: string; verified: boolean; primary: boolean }
|
||||
export interface UiNotifications { prefs: UiNotifCategory[]; timezone: string; timezoneOptions: string[]; windows: UiWindow[]; destinations: UiDestination[] }
|
||||
|
||||
export interface UiConsentItem { id: string; label: string; desc: string; granted: boolean; locked: boolean }
|
||||
export interface UiConsentGroup { id: string; title: string; items: UiConsentItem[] }
|
||||
|
||||
export interface UploadFile { fileName: string; contentType: string; base64: string }
|
||||
|
||||
export interface ProfileData {
|
||||
live: boolean; loading: boolean; error: string | null;
|
||||
account: UiAccount;
|
||||
kyc: UiKyc;
|
||||
notifications: UiNotifications;
|
||||
consent: UiConsentGroup[];
|
||||
// Account
|
||||
reveal: (houseNumber: string) => Promise<boolean>;
|
||||
requestContactChangeOtp: (channel: ContactKind, newValue: string) => Promise<string>;
|
||||
changeContact: (channel: ContactKind, newValue: string, challengeId: string, otp: string) => Promise<void>;
|
||||
// KYC
|
||||
uploadKyc: (slot: KycKey, docTypeId: string | null, file: UploadFile) => Promise<void>;
|
||||
removeKyc: (slot: KycKey) => Promise<void>;
|
||||
// Notifications
|
||||
setPreference: (categoryId: string, channel: NotifChannel, value: boolean) => Promise<void>;
|
||||
setTimezone: (timezone: string) => Promise<void>;
|
||||
setContactWindow: (label: string, patch: { startTime: string; endTime: string; enabled: boolean }) => Promise<void>;
|
||||
addDestination: (channel: DestChannel, label: string, value: string) => Promise<void>;
|
||||
requestDestinationOtp: (id: string) => Promise<string>;
|
||||
verifyDestination: (id: string, challengeId: string, otp: string) => Promise<void>;
|
||||
// Consent
|
||||
setConsent: (groupId: string, itemId: string, granted: boolean) => Promise<void>;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
/* ---- UI copy + static presentation maps ---------------------------------- */
|
||||
|
||||
const SLOT_META: Record<KycKey, { title: string }> = {
|
||||
photo: { title: "Live Photo / Selfie" },
|
||||
id: { title: "Identity Proof" },
|
||||
address: { title: "Address Proof" },
|
||||
};
|
||||
const MIME_LABEL: Record<string, string> = { "application/pdf": "PDF", "image/jpeg": "JPG", "image/png": "PNG" };
|
||||
const friendlyFormats = (mimes: string[]) => mimes.map((m) => MIME_LABEL[m] ?? m).join(", ");
|
||||
const DOC_HINT: Record<string, string> = {
|
||||
aadhaar: "Front & back, masked first 8 digits", pan: "Clear photo of the front",
|
||||
passport: "Photo page, must be valid", voter_id: "Front & back", driving_license: "Front & back, not expired",
|
||||
utility_bill: "Electricity / water, < 3 months old", bank_statement: "First page with address, < 3 months",
|
||||
rent_agreement: "All pages, registered copy",
|
||||
};
|
||||
|
||||
const CATEGORY_DESC: Record<string, string> = {
|
||||
security: "New device, password and 2FA changes",
|
||||
account: "Profile changes and account activity",
|
||||
payments: "Invoices, payment reminders and receipts",
|
||||
meetings: "Upcoming visits and rescheduling",
|
||||
documents: "Document status and re-verification requests",
|
||||
promotions: "Promotions, surveys and newsletters",
|
||||
};
|
||||
const CONSENT_DESC: Record<string, string> = {
|
||||
terms_of_service: "Required to operate your account.",
|
||||
kyc_verification: "Process KYC documents to meet regulatory obligations.",
|
||||
marketing_email: "Offers, product news and updates by email.",
|
||||
marketing_sms: "Offers and reminders by SMS/WhatsApp.",
|
||||
partner_sharing: "Allow vetted partner brokers to process your data.",
|
||||
};
|
||||
|
||||
// The four standard contact-time windows the UI offers. Live windows are these
|
||||
// definitions overlaid with whatever be-crm has stored (times/enabled by label).
|
||||
const WINDOW_DEFS: { label: string; startTime: string; endTime: string }[] = [
|
||||
{ label: "Morning", startTime: "08:00", endTime: "12:00" },
|
||||
{ label: "Afternoon", startTime: "12:00", endTime: "16:00" },
|
||||
{ label: "Evening", startTime: "16:00", endTime: "20:00" },
|
||||
{ label: "Night", startTime: "20:00", endTime: "22:00" },
|
||||
];
|
||||
|
||||
const GRADIENTS = [
|
||||
"linear-gradient(135deg,#fda913,#fd6d13)", "linear-gradient(135deg,#6366f1,#8b5cf6)",
|
||||
"linear-gradient(135deg,#06b6d4,#3b82f6)", "linear-gradient(135deg,#10b981,#059669)",
|
||||
"linear-gradient(135deg,#f43f5e,#ec4899)", "linear-gradient(135deg,#8b5cf6,#d946ef)",
|
||||
];
|
||||
const gradientFor = (id: string) => GRADIENTS[[...id].reduce((a, c) => a + c.charCodeAt(0), 0) % GRADIENTS.length];
|
||||
const initialsOf = (name: string) =>
|
||||
name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase() || "?";
|
||||
|
||||
/* ---- be-crm DTO types ---------------------------------------------------- */
|
||||
|
||||
interface AccountReadDTO {
|
||||
principalId: string;
|
||||
allotmentNo: string | null; plot: string | null; sector: string | null; pocket: string | null;
|
||||
category: string | null; size: string | null; isAllottee: boolean;
|
||||
relationship: string | null; allotteeName: string | null;
|
||||
email: string | null; mobile: string | null; whatsapp: string | null;
|
||||
pan: string | null; aadhaar: string | null;
|
||||
revealed: boolean; memberSince: string; status: "active" | "suspended"; kycCompletionPct: number;
|
||||
}
|
||||
interface KycSlotDTO { slot: KycKey; docTypeId: string | null; fileName: string | null; status: KycStatus; rejectReason: string | null; uploadedAt: string | null }
|
||||
interface KycListDTO { slots: KycSlotDTO[]; docTypes: { id: string; label: string; use: string; formats: string[]; maxMb: number }[]; completionPct: number }
|
||||
interface PrefDTO { categoryId: string; label: string; locked: boolean; email: boolean; sms: boolean; whatsapp: boolean; push: boolean }
|
||||
interface WindowDTO { id: string; label: string; startTime: string; endTime: string; enabled: boolean }
|
||||
interface DestDTO { id: string; channel: DestChannel; label: string | null; value: string; verified: boolean; isPrimary: boolean }
|
||||
interface NotificationsGetDTO { preferences: PrefDTO[]; timezone: string; timezoneOptions: string[]; windows: WindowDTO[]; destinations: DestDTO[] }
|
||||
interface ConsentItemDTO { itemId: string; label: string; statutory: boolean; granted: boolean }
|
||||
interface ConsentGroupDTO { groupId: string; label: string; items: ConsentItemDTO[] }
|
||||
|
||||
/* ---- shared derivations -------------------------------------------------- */
|
||||
|
||||
function kycNote(status: KycStatus, rejectReason: string | null): string | undefined {
|
||||
if (rejectReason) return rejectReason;
|
||||
if (status === "uploaded") return "Under review";
|
||||
if (status === "verified") return "Verified";
|
||||
return undefined;
|
||||
}
|
||||
function mergeWindows(stored: WindowDTO[]): UiWindow[] {
|
||||
const byLabel = new Map(stored.map((w) => [w.label, w]));
|
||||
return WINDOW_DEFS.map((def) => {
|
||||
const w = byLabel.get(def.label);
|
||||
const startTime = w?.startTime ?? def.startTime;
|
||||
const endTime = w?.endTime ?? def.endTime;
|
||||
return { id: w?.id ?? def.label, label: def.label, startTime, endTime, range: `${startTime} – ${endTime}`, enabled: w?.enabled ?? false };
|
||||
});
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Mock implementation (no Shell configured) */
|
||||
/* ======================================================================== */
|
||||
|
||||
function useMockProfile(): ProfileData {
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [contacts, setContacts] = useState({ email: seedUser.email.full, mobile: seedUser.mobile.full, whatsapp: seedUser.whatsapp.full });
|
||||
const [slots, setSlots] = useState<UiKycSlot[]>(() =>
|
||||
seedKyc.slots.map((s) => ({ key: s.key, title: SLOT_META[s.key].title, required: s.required, docId: s.docId, fileName: s.fileName, status: s.status, note: s.note })));
|
||||
const [prefs, setPrefs] = useState<UiNotifCategory[]>(() =>
|
||||
seedPrefs.map((c) => ({ id: c.id, label: c.label, desc: c.desc, locked: Boolean(c.locked), email: c.email, sms: c.sms, whatsapp: c.whatsapp, push: c.push })));
|
||||
const [timezone, setTz] = useState(seedTz[0]);
|
||||
const [windows, setWindows] = useState<UiWindow[]>(() =>
|
||||
seedWindows.map((w) => ({ id: w.id, label: w.label, range: w.range, startTime: w.range.split(" – ")[0], endTime: w.range.split(" – ")[1], enabled: w.enabled })));
|
||||
const [destinations, setDestinations] = useState<UiDestination[]>(() =>
|
||||
seedDests.map((d) => ({ id: d.id, channel: d.channel, label: d.label, value: d.value, verified: d.verified, primary: d.primary })));
|
||||
const [consent, setConsent] = useState<UiConsentGroup[]>(() =>
|
||||
seedConsent.map((g) => ({ id: g.id, title: g.title, items: g.items.map((it) => ({ id: it.id, label: it.label, desc: it.desc, granted: it.granted, locked: Boolean(it.locked) })) })));
|
||||
|
||||
const docTypes: UiDocType[] = useMemo(() =>
|
||||
seedDocTypes.map((d) => ({ id: d.id, label: d.label, use: d.use, hint: d.hint, formats: d.formats, maxMb: d.maxMb })), []);
|
||||
|
||||
const account: UiAccount = {
|
||||
principalId: seedUser.id, name: seedUser.name, role: seedUser.role, initials: seedUser.initials,
|
||||
avatarGradient: seedUser.avatarGradient, memberSince: seedUser.memberSince, status: seedUser.status,
|
||||
allotmentNo: seedUser.allotmentNo, plot: seedUser.plot, sector: seedUser.sector, pocket: seedUser.pocket,
|
||||
category: seedUser.category, size: seedUser.size, isAllottee: seedUser.isAllottee,
|
||||
relationship: seedUser.relationship, allotteeName: seedUser.allotteeName,
|
||||
email: revealed ? contacts.email : seedUser.email.masked,
|
||||
mobile: revealed ? contacts.mobile : seedUser.mobile.masked,
|
||||
whatsapp: revealed ? contacts.whatsapp : seedUser.whatsapp.masked,
|
||||
pan: revealed ? seedUser.pan.full : seedUser.pan.masked,
|
||||
aadhaar: revealed ? seedUser.aadhaar.full : seedUser.aadhaar.masked,
|
||||
revealed, kycCompletionPct: Math.round((slots.filter((s) => s.status !== "missing").length / slots.length) * 100),
|
||||
};
|
||||
|
||||
const reveal = useCallback(async (houseNumber: string) => {
|
||||
const ok = houseNumber.trim() === seedUser.plot;
|
||||
if (ok) setRevealed(true);
|
||||
return ok;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
live: false, loading: false, error: null, account,
|
||||
kyc: { slots, docTypes, completionPct: account.kycCompletionPct },
|
||||
notifications: { prefs, timezone, timezoneOptions: seedTz, windows, destinations },
|
||||
consent,
|
||||
reveal,
|
||||
requestContactChangeOtp: async () => "mock-challenge",
|
||||
changeContact: async (channel, newValue) => { setContacts((c) => ({ ...c, [channel]: newValue })); },
|
||||
uploadKyc: async (slot, docTypeId, file) => {
|
||||
setSlots((s) => s.map((x) => x.key === slot ? { ...x, docId: docTypeId ?? x.docId, fileName: file.fileName, status: "uploaded", note: "Under review" } : x));
|
||||
},
|
||||
removeKyc: async (slot) => {
|
||||
setSlots((s) => s.map((x) => x.key === slot ? { ...x, fileName: null, docId: slot === "photo" ? null : x.docId, status: "missing", note: undefined } : x));
|
||||
},
|
||||
setPreference: async (categoryId, channel, value) => {
|
||||
setPrefs((p) => p.map((c) => c.id === categoryId ? { ...c, [channel]: value } : c));
|
||||
},
|
||||
setTimezone: async (tz) => setTz(tz),
|
||||
setContactWindow: async (label, patch) => {
|
||||
setWindows((w) => w.map((x) => x.label === label ? { ...x, ...patch, range: `${patch.startTime} – ${patch.endTime}` } : x));
|
||||
},
|
||||
addDestination: async (channel, label, value) => {
|
||||
setDestinations((d) => [...d, { id: `dest_${d.length + 1}`, channel, label, value, verified: false, primary: false }]);
|
||||
},
|
||||
requestDestinationOtp: async () => "mock-challenge",
|
||||
verifyDestination: async (id) => { setDestinations((d) => d.map((x) => x.id === id ? { ...x, verified: true } : x)); },
|
||||
setConsent: async (groupId, itemId, granted) => {
|
||||
setConsent((gs) => gs.map((g) => g.id !== groupId ? g : { ...g, items: g.items.map((it) => it.id === itemId ? { ...it, granted } : it) }));
|
||||
},
|
||||
refetch: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
/* ======================================================================== */
|
||||
/* Live implementation (be-crm data door) */
|
||||
/* ======================================================================== */
|
||||
|
||||
function useLiveProfile(): ProfileData {
|
||||
const { sdk } = useAppShell();
|
||||
const { user, context } = useAuth();
|
||||
|
||||
const accountQ = useQuery<AccountReadDTO>("crm.account.read");
|
||||
const kycQ = useQuery<KycListDTO>("crm.kyc.list");
|
||||
const notifQ = useQuery<NotificationsGetDTO>("crm.notifications.get");
|
||||
const consentQ = useQuery<ConsentGroupDTO[]>("crm.consent.list");
|
||||
|
||||
const refetch = useCallback(() => { accountQ.refetch(); kycQ.refetch(); notifQ.refetch(); consentQ.refetch(); }, [accountQ, kycQ, notifQ, consentQ]);
|
||||
const cmd = useCallback(async (action: string, variables: Record<string, unknown>) => {
|
||||
await sdk.command(action, variables); refetch();
|
||||
}, [sdk, refetch]);
|
||||
|
||||
const a = accountQ.data;
|
||||
const displayName = (user?.displayName?.trim() || a?.allotteeName?.trim() || user?.email || "My account");
|
||||
// Role for the header: derive from the profile itself, not a hardcoded label.
|
||||
const role = a?.isAllottee ? "Property Owner" : (a?.relationship?.trim() || "Representative");
|
||||
const roleFromContext = (context as { scope?: { role?: string } } | null)?.scope?.role;
|
||||
|
||||
const account: UiAccount = {
|
||||
principalId: a?.principalId ?? user?.id ?? "",
|
||||
name: displayName, role: roleFromContext || role, initials: initialsOf(displayName),
|
||||
avatarGradient: gradientFor(a?.principalId ?? user?.id ?? "x"),
|
||||
memberSince: a?.memberSince ?? "—", status: a?.status ?? "active",
|
||||
allotmentNo: a?.allotmentNo ?? null, plot: a?.plot ?? null, sector: a?.sector ?? null,
|
||||
pocket: a?.pocket ?? null, category: a?.category ?? null, size: a?.size ?? null,
|
||||
isAllottee: a?.isAllottee ?? false, relationship: a?.relationship ?? null, allotteeName: a?.allotteeName ?? null,
|
||||
email: a?.email ?? "—", mobile: a?.mobile ?? "—", whatsapp: a?.whatsapp ?? "—",
|
||||
pan: a?.pan ?? "—", aadhaar: a?.aadhaar ?? "—",
|
||||
revealed: a?.revealed ?? false, kycCompletionPct: a?.kycCompletionPct ?? 0,
|
||||
};
|
||||
|
||||
const kyc: UiKyc = useMemo(() => {
|
||||
const d = kycQ.data;
|
||||
const slots: UiKycSlot[] = (d?.slots ?? []).map((s) => ({
|
||||
key: s.slot, title: SLOT_META[s.slot].title, required: true,
|
||||
docId: s.docTypeId, fileName: s.fileName, status: s.status, note: kycNote(s.status, s.rejectReason),
|
||||
}));
|
||||
const docTypes: UiDocType[] = (d?.docTypes ?? []).map((t) => ({
|
||||
id: t.id, label: t.label, use: t.use, hint: DOC_HINT[t.id] ?? "", formats: friendlyFormats(t.formats), maxMb: t.maxMb,
|
||||
}));
|
||||
return { slots, docTypes, completionPct: d?.completionPct ?? account.kycCompletionPct };
|
||||
}, [kycQ.data, account.kycCompletionPct]);
|
||||
|
||||
const notifications: UiNotifications = useMemo(() => {
|
||||
const d = notifQ.data;
|
||||
const prefs: UiNotifCategory[] = (d?.preferences ?? []).map((p) => ({
|
||||
id: p.categoryId, label: p.label, desc: CATEGORY_DESC[p.categoryId] ?? "", locked: p.locked,
|
||||
email: p.email, sms: p.sms, whatsapp: p.whatsapp, push: p.push,
|
||||
}));
|
||||
const destinations: UiDestination[] = (d?.destinations ?? []).map((x) => ({
|
||||
id: x.id, channel: x.channel, label: x.label ?? x.channel, value: x.value, verified: x.verified, primary: x.isPrimary,
|
||||
}));
|
||||
return {
|
||||
prefs, timezone: d?.timezone ?? "Asia/Kolkata",
|
||||
timezoneOptions: d?.timezoneOptions ?? ["Asia/Kolkata"],
|
||||
windows: mergeWindows(d?.windows ?? []), destinations,
|
||||
};
|
||||
}, [notifQ.data]);
|
||||
|
||||
const consent: UiConsentGroup[] = useMemo(() => (consentQ.data ?? []).map((g) => ({
|
||||
id: g.groupId, title: g.label,
|
||||
items: g.items.map((it) => ({ id: it.itemId, label: it.label, desc: CONSENT_DESC[it.itemId] ?? "", granted: it.granted, locked: it.statutory })),
|
||||
})), [consentQ.data]);
|
||||
|
||||
const reveal = useCallback(async (houseNumber: string): Promise<boolean> => {
|
||||
try {
|
||||
await sdk.command("crm.account.revealField", { houseNumber });
|
||||
accountQ.refetch();
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}, [sdk, accountQ]);
|
||||
|
||||
return {
|
||||
live: true,
|
||||
loading: accountQ.loading || kycQ.loading || notifQ.loading || consentQ.loading,
|
||||
error: (accountQ.error ?? kycQ.error ?? notifQ.error ?? consentQ.error)?.message ?? null,
|
||||
account, kyc, notifications, consent, reveal,
|
||||
requestContactChangeOtp: async (channel, newValue) => {
|
||||
const r = await sdk.command<{ challengeId: string }>("crm.account.requestContactChangeOtp", { channel, newValue });
|
||||
return r.challengeId;
|
||||
},
|
||||
changeContact: (channel, newValue, challengeId, otp) =>
|
||||
cmd("crm.account.changeContact", { channel, newValue, challengeId, otp }),
|
||||
uploadKyc: (slot, docTypeId, file) =>
|
||||
cmd("crm.kyc.upload", { slot, ...(docTypeId ? { docTypeId } : {}), file }),
|
||||
removeKyc: (slot) => cmd("crm.kyc.remove", { slot }),
|
||||
setPreference: (categoryId, channel, value) => cmd("crm.notifications.setPreference", { categoryId, channel, value }),
|
||||
setTimezone: (timezone) => cmd("crm.notifications.setTimezone", { timezone }),
|
||||
setContactWindow: (label, patch) => cmd("crm.notifications.setContactWindow", { label, ...patch }),
|
||||
addDestination: (channel, label, value) => cmd("crm.notifications.addDestination", { channel, label, value }),
|
||||
requestDestinationOtp: async (id) => {
|
||||
const r = await sdk.command<{ challengeId?: string }>("crm.notifications.verifyDestination", { id });
|
||||
return r.challengeId ?? "";
|
||||
},
|
||||
verifyDestination: (id, challengeId, otp) => cmd("crm.notifications.verifyDestination", { id, challengeId, otp }),
|
||||
setConsent: (groupId, itemId, granted) => cmd("crm.consent.set", { groupId, itemId, granted }),
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- public hook: pick the implementation at module-config time --------- */
|
||||
|
||||
const SHELL = isShellConfigured();
|
||||
|
||||
export function useProfileData(): ProfileData {
|
||||
// `SHELL` is constant for the life of the bundle (NEXT_PUBLIC_* is build-time),
|
||||
// so the same hook path runs every render — Rules-of-Hooks safe.
|
||||
return SHELL ? useLiveProfile() : useMockProfile();
|
||||
}
|
||||