Initial commit: LynkedUp Fusion site

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Goutam
2026-07-17 14:51:13 +05:30
commit 74e52e9dcd
53 changed files with 9703 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# Dependencies
node_modules/
# Build output
dist/
dist-ssr/
# Vite cache
.vite/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment variables
.env
.env.*
!.env.example
# Editor / OS
.DS_Store
.idea/
.vscode/*
!.vscode/extensions.json
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Package manager lockfiles state
.npm
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.vercel
.env*
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
.vercel
+335
View File
@@ -0,0 +1,335 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The First 48 Hours After Hail: A Field Checklist That Protects the Claim - LynkedUp Pro Blog</title>
<meta name="description" content="The window between the storm passing and the adjuster arriving decides whether your evidence holds up. Here's exactly what to capture.">
<meta property="og:title" content="The First 48 Hours After Hail: A Field Checklist That Protects the Claim - LynkedUp Pro Blog">
<meta property="og:description" content="The window between the storm passing and the adjuster arriving decides whether your evidence holds up. Here's exactly what to capture.">
<meta property="og:type" content="website">
<meta property="og:image" content="../../og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The First 48 Hours After Hail: A Field Checklist That Protects the Claim - LynkedUp Pro Blog">
<meta name="twitter:description" content="The window between the storm passing and the adjuster arriving decides whether your evidence holds up. Here's exactly what to capture.">
<meta name="twitter:image" content="../../og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "The First 48 Hours After Hail: A Field Checklist That Protects the Claim",
"author": {"@type": "Organization", "name": "LynkedUp Pro"},
"publisher": {"@type": "Organization", "name": "LynkedUp Pro"}
}
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="../../index.html#product">Product</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#intel">Intelligence</a>
<a href="../../index.html#pricing">Pricing</a>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../faq.html">FAQ</a>
</nav>
<a class="btn solid" href="../../index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<nav class="rail" id="rail" aria-label="Section index">
<div class="rail-track" id="railTrack">
<div class="rail-carriage" id="railCar"><span class="rail-read" id="railRead"></span></div>
</div>
</nav>
<main id="top">
<!-- ============ POST HEADER ============ -->
<div class="page-hero" id="intro" data-rail="01 &middot; INTRO">
<div class="wrap">
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>STORM RESPONSE</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Storm response</span>
<h1>The First 48 Hours After Hail: A Field Checklist That Protects the Claim</h1>
<p class="post-meta">
<span>LynkedUp Pro Team</span><i></i><span>Jun 9, 2026</span><i></i><span>7 min read</span>
</p>
</div>
<figure class="post-cover reveal">
<img src="../../img/props/Hail_Damaged_Shingles.jpg" alt="Close up of hail damaged shingles" loading="eager" width="1600" height="762">
</figure>
</div>
</div>
<div class="wrap">
<div class="post-body reveal">
<p>The storm is the easy part. It shows up, does the damage, and moves on. What happens in the 48 hours after it passes decides whether every claim in its path gets approved on the first pass, or drags into a second visit and a supplemental fight.</p>
<p>Crews that treat those two days as a documentation sprint &mdash; not just a canvassing sprint &mdash; close faster and get paid more of what they bid. Here's the order that works.</p>
<section id="hour-0-6" data-rail="02 &middot; HOUR 0&ndash;6">
<h2>Hour 0&ndash;6: Confirm and Document the Event Itself</h2>
<p>Before a single roof gets inspected, lock down the storm data you'll need for every claim in the territory: verified hail size, wind speed, and the exact swath the cell tracked over. Adjusters trust radar-verified data far more than a homeowner's memory of "it was big."</p>
<ul>
<li>Pull the radar-verified hail swath and impact size for your zone.</li>
<li>Screenshot or export the report with a timestamp &mdash; you'll attach this to every claim filed from this event.</li>
<li>Flag every property already in your pipeline that falls inside the swath.</li>
</ul>
</section>
<section id="hour-6-24" data-rail="03 &middot; HOUR 6&ndash;24">
<h2>Hour 6&ndash;24: Get Eyes on Every Roof in the Path</h2>
<p>This is the window where speed compounds. The first crew to reach a property with a phone-based scan usually walks away with the signature, because nobody else has shown up with a measured bid yet.</p>
<div class="risks">
<div class="risk-card reveal">
<span class="num big red">1.75 in</span>
<h3>Hail size, verified</h3>
<p>Radar-confirmed impact diameter for the April 22 supercell used in our reference claim.</p>
<span class="stamp-tag danger">&#10005; HAIL VERIFIED</span>
</div>
<div class="risk-card reveal">
<span class="num big amber">41</span>
<h3>Impact points, facet F2</h3>
<p>Individually mapped and photographed &mdash; the density that turns "some wear" into an approved claim.</p>
<span class="stamp-tag caution">&#9888; MODERATE&ndash;HIGH</span>
</div>
<div class="risk-card reveal">
<span class="num big sky">0.6s</span>
<h3>Scan-to-report time</h3>
<p>From phone capture to a measured, damage-flagged estimate, ready to hand to the homeowner.</p>
<span class="stamp-tag ok">&#10003; ON SITE</span>
</div>
</div>
</section>
<blockquote class="pull-quote">
A claim doesn't get denied because the damage wasn't real. It gets denied because the damage wasn't proven.
</blockquote>
<section id="hour-24-48" data-rail="04 &middot; HOUR 24&ndash;48">
<h2>Hour 24&ndash;48: Build the Evidence Pack Before the Adjuster Calls</h2>
<p>By the time an adjuster schedules a visit, you want the packet already assembled: dated photos of every impact, a facet-by-facet damage map, and the shingle class and age on record. Adjusters move fast through files that are already organized in the format carriers expect &mdash; and slow through everything else.</p>
<ul>
<li>Group photos by facet, not by folder date.</li>
<li>Include a wide shot of each slope alongside the close-up impact photos.</li>
<li>Note shingle class, roof age, and any prior repair history up front.</li>
<li>Timestamp and geo-tag everything automatically &mdash; don't rely on manually renaming files at 11pm.</li>
</ul>
</section>
<section id="what-adjusters-look-for" data-rail="05 &middot; WHAT ADJUSTERS WANT">
<h2>What Adjusters Actually Look For</h2>
<p>Adjusters aren't looking to deny a legitimate claim &mdash; they're looking for a reason to approve it quickly and move to the next file on their desk. Give them a complete, dated, facet-mapped packet and most claims in a verified hail path close without a second roof visit.</p>
</section>
</div>
<div class="founder-card terms-cta reveal">
<span class="stamp-tag caution">&#9888; STORM WATCH ACTIVE</span>
<h2>Get <em>hail alerts</em> for your territory.</h2>
<p>Radar-verified swaths land in your inbox the morning after the storm, so your crews are the first ones knocking.</p>
<div class="cta-alt">
<a class="btn solid" href="../../index.html#book">Book a demo</a>
<a class="btn" href="../../index.html#intel">See the intelligence map</a>
</div>
</div>
<div class="post-foot-nav">
<a href="../">&larr; All field notes</a>
<a href="../why-claims-get-denied/">Next: Why Adjusters Reject 40% of Claims &rarr;</a>
</div>
</div>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="../../index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="../../index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="../../index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="../../index.html#hero">Instant estimate</a>
<a href="../../index.html#intel">Intelligence map</a>
<a href="../../index.html#product">For contractors</a>
<a href="../../index.html#industries">For homeowners</a>
<a href="../../index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#book">Book a demo</a>
<a href="../../index.html#book">Careers</a>
<a href="../../index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="../../faq.html">FAQ</a>
<a href="../../index.html">Help center</a>
<a href="../../index.html">Security</a>
<a href="../../index.html">Privacy policy</a>
<a href="../../terms/">Terms of service</a>
<a href="../../index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="../../src/blog.js"></script>
</body>
</html>
+336
View File
@@ -0,0 +1,336 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts - LynkedUp Pro Blog</title>
<meta name="description" content="Every manual follow-up and missing document chips away at your margin. Here's exactly where the 34% goes, and how to keep more of it.">
<meta property="og:title" content="The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts - LynkedUp Pro Blog">
<meta property="og:description" content="Every manual follow-up and missing document chips away at your margin. Here's exactly where the 34% goes, and how to keep more of it.">
<meta property="og:type" content="website">
<meta property="og:image" content="../../og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts - LynkedUp Pro Blog">
<meta name="twitter:description" content="Every manual follow-up and missing document chips away at your margin. Here's exactly where the 34% goes, and how to keep more of it.">
<meta name="twitter:image" content="../../og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts",
"author": {"@type": "Organization", "name": "LynkedUp Pro"},
"publisher": {"@type": "Organization", "name": "LynkedUp Pro"}
}
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="../../index.html#product">Product</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#intel">Intelligence</a>
<a href="../../index.html#pricing">Pricing</a>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../faq.html">FAQ</a>
</nav>
<a class="btn solid" href="../../index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<nav class="rail" id="rail" aria-label="Section index">
<div class="rail-track" id="railTrack">
<div class="rail-carriage" id="railCar"><span class="rail-read" id="railRead"></span></div>
</div>
</nav>
<main id="top">
<!-- ============ POST HEADER ============ -->
<div class="page-hero" id="intro" data-rail="01 &middot; INTRO">
<div class="wrap">
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>OPERATIONS</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Operations</span>
<h1>The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts</h1>
<p class="post-meta">
<span>LynkedUp Pro Team</span><i></i><span>Jun 2, 2026</span><i></i><span>6 min read</span>
</p>
</div>
<figure class="post-cover reveal">
<img src="../../img/props/Storm_Worn_Roof.jpg" alt="Weathered roof shingles after storm exposure" loading="eager" width="1600" height="762">
</figure>
</div>
</div>
<div class="wrap">
<div class="post-body reveal">
<p>Ask any owner what their margin is and they'll give you a number from the estimate: 34%, 30%, whatever the spreadsheet says. Ask them what they actually collected on the last ten jobs, and the number is usually lower &mdash; sometimes a lot lower. The gap isn't bad pricing. It's paperwork.</p>
<p>Every manual follow-up, every missing project detail, every proposal re-typed because a price changed, every job photo that never made it off someone's phone &mdash; each one is small. Stacked across a season, they're the difference between a good year and a break-even one.</p>
<section id="hidden-tax" data-rail="02 &middot; HIDDEN TAX">
<h2>The Hidden Tax on Every Job</h2>
<p>We pulled the timeline on a real hail claim &mdash; same lead, same roof, run two ways: once through a manual file (voicemail, ladder measurement, typed proposal, mailed revisions), and once through a system that captures, measures, and files automatically.</p>
<div class="risks">
<div class="risk-card reveal">
<span class="num big red">47 &rarr; 15</span>
<h3>Days to close</h3>
<p>The manual file sat on a second site visit and a re-typed revision. The automated file never left the queue.</p>
<span class="stamp-tag danger">&#10005; 32 DAYS LOST</span>
</div>
<div class="risk-card reveal">
<span class="num big amber">12.5 &rarr; 0.8</span>
<h3>Admin hours</h3>
<p>Nearly a full day of phone tag, retyping, and chasing signatures &mdash; on one job.</p>
<span class="stamp-tag caution">&#9888; PER FILE</span>
</div>
<div class="risk-card reveal">
<span class="num big sky">$4,120</span>
<h3>Margin at risk</h3>
<p>What the manual file was on pace to lose before the job even reached production.</p>
<span class="stamp-tag ok">&#10003; HELD, AUTOMATED</span>
</div>
</div>
</section>
<section id="where-it-leaks" data-rail="03 &middot; WHERE IT LEAKS">
<h2>Where the Margin Actually Leaks</h2>
<p>It rarely leaks in one dramatic place. It leaks in the same three spots, on almost every job:</p>
<ul>
<li><strong>The second site visit.</strong> A measurement taken with a tape and a ladder isn't wrong, but it isn't documented well enough to survive a dispute. When the insurer asks for evidence, someone drives back out.</li>
<li><strong>The re-typed proposal.</strong> A homeowner disputes one line item, and the whole document gets retyped from scratch because there's no live source of truth &mdash; just a Word file on someone's desktop.</li>
<li><strong>The follow-up that never happens.</strong> Leads go cold not because reps are lazy, but because there's no system nudging the next action. "I'll call them back" is not a workflow.</li>
</ul>
</section>
<blockquote class="pull-quote">
The roof doesn't cost you the margin. The gap between the site visit and the signed proposal does.
</blockquote>
<section id="why-follow-up-fails" data-rail="04 &middot; WHY IT FAILS">
<h2>Why "We'll Follow Up" Doesn't Scale</h2>
<p>Every sales team believes it follows up consistently. Almost none of them actually do, because follow-up that lives in someone's memory competes with a truck full of other jobs, a crew that needs scheduling, and a phone that won't stop ringing.</p>
<p>The fix isn't a more disciplined team. It's removing the step where a human has to remember anything. A missed call that auto-triggers a text. A scan that becomes a proposal without anyone opening a word processor. A signed job that files its own claim paperwork. None of that requires better people &mdash; it requires a system that does the remembering.</p>
</section>
<section id="fix" data-rail="05 &middot; THE FIX">
<h2>What Actually Fixes It</h2>
<p>You don't need to overhaul how your crews work. You need to close the three leaks above:</p>
<ol>
<li>Capture the scan and the measurement in one visit, on the phone that's already in the truck.</li>
<li>Generate the proposal from that same data, so a disputed line item is an edit, not a re-type.</li>
<li>Let the system trigger the next follow-up automatically, on a schedule a human doesn't have to hold in their head.</li>
</ol>
<p>None of it is exotic. It's just the difference between a job file that works for you and one you have to work for.</p>
</section>
</div>
<div class="founder-card terms-cta reveal">
<span class="stamp-tag ok">&#10003; SEE IT ON YOUR OWN NUMBERS</span>
<h2>Run the <em>same comparison</em> on your last job.</h2>
<p>Book a 30-minute walkthrough and we'll build the before/after timeline using one of your own recent files.</p>
<div class="cta-alt">
<a class="btn solid" href="../../index.html#book">Book a demo</a>
<a class="btn" href="../../index.html#chaos">See the Cost of Chaos</a>
</div>
</div>
<div class="post-foot-nav">
<a href="../">&larr; All field notes</a>
<a href="../48-hour-storm-checklist/">Next: The First 48 Hours After Hail &rarr;</a>
</div>
</div>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="../../index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="../../index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="../../index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="../../index.html#hero">Instant estimate</a>
<a href="../../index.html#intel">Intelligence map</a>
<a href="../../index.html#product">For contractors</a>
<a href="../../index.html#industries">For homeowners</a>
<a href="../../index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#book">Book a demo</a>
<a href="../../index.html#book">Careers</a>
<a href="../../index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="../../faq.html">FAQ</a>
<a href="../../index.html">Help center</a>
<a href="../../index.html">Security</a>
<a href="../../index.html">Privacy policy</a>
<a href="../../terms/">Terms of service</a>
<a href="../../index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="../../src/blog.js"></script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blog - LynkedUp Pro | Field notes for contractors</title>
<meta name="description" content="Field notes for contractors: estimating, storm claims, crew ops, and the paperwork that quietly eats your margin.">
<meta property="og:title" content="Blog - LynkedUp Pro | Field notes for contractors">
<meta property="og:description" content="Field notes for contractors: estimating, storm claims, crew ops, and the paperwork that quietly eats your margin.">
<meta property="og:type" content="website">
<meta property="og:image" content="../og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Blog - LynkedUp Pro | Field notes for contractors">
<meta name="twitter:description" content="Field notes for contractors: estimating, storm claims, crew ops, and the paperwork that quietly eats your margin.">
<meta name="twitter:image" content="../og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="../index.html#product">Product</a>
<a href="../index.html#chaos">Why it works</a>
<a href="../index.html#intel">Intelligence</a>
<a href="../index.html#pricing">Pricing</a>
<a href="../index.html#founder">About</a>
<a href="../blog/">Blog</a>
<a href="../faq.html">FAQ</a>
</nav>
<a class="btn solid" href="../index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<main id="top">
<!-- ============ FIELD NOTES · INTRO ============ -->
<div class="page-hero" id="intro">
<div class="wrap">
<span class="sheet num" aria-hidden="true">01</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 01 &middot; FIELD NOTES</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Field notes</span>
<h1>Straight talk for people who <em>build things.</em></h1>
<p>Notes from the field on estimating, storm claims, crew ops, and the paperwork that quietly eats your margin. No fluff &mdash; just what we've seen work on real jobs.</p>
</div>
</div>
</div>
<!-- ============ POSTS GRID ============ -->
<section id="posts">
<div class="wrap">
<div class="blog-grid">
<a class="feat blog-card reveal" href="./cost-of-paperwork/" aria-label="Read: The Real Cost of Paperwork">
<div class="feat-media">
<img class="feat-thumb" src="../img/props/Storm_Worn_Roof.jpg" alt="Weathered roof shingles after storm exposure" loading="lazy" width="900" height="510">
<span class="cap">OPERATIONS &middot; JUN 2, 2026 &middot; 6 MIN READ</span>
</div>
<div class="feat-body">
<h3>The Real Cost of Paperwork: Why Contractors Lose Margin Before the Job Even Starts</h3>
<p>Every manual follow-up, delayed estimate, and missing document chips away at the number that actually matters. Here's exactly where the 34% goes.</p>
<span class="feat-link">Read the field note <span aria-hidden="true">&#9656;</span></span>
</div>
</a>
<a class="feat blog-card reveal" href="./48-hour-storm-checklist/" aria-label="Read: The First 48 Hours After Hail">
<div class="feat-media">
<img class="feat-thumb" src="../img/props/Hail_Damaged_Shingles.jpg" alt="Close up of hail damaged shingles" loading="lazy" width="900" height="510">
<span class="cap">STORM RESPONSE &middot; JUN 9, 2026 &middot; 7 MIN READ</span>
</div>
<div class="feat-body">
<h3>The First 48 Hours After Hail: A Field Checklist That Protects the Claim</h3>
<p>The window between "storm passed" and "adjuster arrives" decides whether your evidence holds up. Here's exactly what to capture, and in what order.</p>
<span class="feat-link">Read the field note <span aria-hidden="true">&#9656;</span></span>
</div>
</a>
<a class="feat blog-card reveal" href="./why-claims-get-denied/" aria-label="Read: Why Adjusters Reject 40 Percent of Roof Claims">
<div class="feat-media">
<img class="feat-thumb" src="../img/props/Broken_Roof_Surface.jpg" alt="Damaged roof membrane surface" loading="lazy" width="900" height="510">
<span class="cap">CLAIMS &middot; JUN 16, 2026 &middot; 5 MIN READ</span>
</div>
<div class="feat-body">
<h3>Why Adjusters Reject 40% of Roof Claims (And How to Fix It)</h3>
<p>"Lack of maintenance" is the single most common denial reason in the industry &mdash; and it's almost always a documentation problem, not a roof problem.</p>
<span class="feat-link">Read the field note <span aria-hidden="true">&#9656;</span></span>
</div>
</a>
</div>
<p class="blog-foot-note tag reveal">MORE FIELD NOTES ADDED REGULARLY</p>
</div>
</section>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="../index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="../index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="../index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="../index.html#hero">Instant estimate</a>
<a href="../index.html#intel">Intelligence map</a>
<a href="../index.html#product">For contractors</a>
<a href="../index.html#industries">For homeowners</a>
<a href="../index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="../index.html#founder">About</a>
<a href="../blog/">Blog</a>
<a href="../index.html#chaos">Why it works</a>
<a href="../index.html#book">Book a demo</a>
<a href="../index.html#book">Careers</a>
<a href="../index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="../faq.html">FAQ</a>
<a href="../index.html">Help center</a>
<a href="../index.html">Security</a>
<a href="../index.html">Privacy policy</a>
<a href="../terms/">Terms of service</a>
<a href="../index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="../src/blog.js"></script>
</body>
</html>
+334
View File
@@ -0,0 +1,334 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Why Adjusters Reject 40% of Roof Claims (And How to Fix It) - LynkedUp Pro Blog</title>
<meta name="description" content="Lack of maintenance is the most common denial reason in the industry, and it's almost always a documentation problem, not a roof problem.">
<meta property="og:title" content="Why Adjusters Reject 40% of Roof Claims (And How to Fix It) - LynkedUp Pro Blog">
<meta property="og:description" content="Lack of maintenance is the most common denial reason in the industry, and it's almost always a documentation problem, not a roof problem.">
<meta property="og:type" content="website">
<meta property="og:image" content="../../og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Why Adjusters Reject 40% of Roof Claims (And How to Fix It) - LynkedUp Pro Blog">
<meta name="twitter:description" content="Lack of maintenance is the most common denial reason in the industry, and it's almost always a documentation problem, not a roof problem.">
<meta name="twitter:image" content="../../og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Why Adjusters Reject 40% of Roof Claims (And How to Fix It)",
"author": {"@type": "Organization", "name": "LynkedUp Pro"},
"publisher": {"@type": "Organization", "name": "LynkedUp Pro"}
}
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="../../index.html#product">Product</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#intel">Intelligence</a>
<a href="../../index.html#pricing">Pricing</a>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../faq.html">FAQ</a>
</nav>
<a class="btn solid" href="../../index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<nav class="rail" id="rail" aria-label="Section index">
<div class="rail-track" id="railTrack">
<div class="rail-carriage" id="railCar"><span class="rail-read" id="railRead"></span></div>
</div>
</nav>
<main id="top">
<!-- ============ POST HEADER ============ -->
<div class="page-hero" id="intro" data-rail="01 &middot; INTRO">
<div class="wrap">
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>CLAIMS</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Claims</span>
<h1>Why Adjusters Reject 40% of Roof Claims (And How to Fix It)</h1>
<p class="post-meta">
<span>LynkedUp Pro Team</span><i></i><span>Jun 16, 2026</span><i></i><span>5 min read</span>
</p>
</div>
<figure class="post-cover reveal">
<img src="../../img/props/Broken_Roof_Surface.jpg" alt="Damaged roof membrane surface" loading="eager" width="1600" height="762">
</figure>
</div>
</div>
<div class="wrap">
<div class="post-body reveal">
<p>"Lack of maintenance" shows up on more denial letters than any other reason in the industry. It's frustrating for homeowners and worse for the contractor who now has to either re-fight the claim or eat the cost of a job that was never in question.</p>
<p>Here's the part that should change how you run every job going forward: it's almost never actually about maintenance. It's about proof.</p>
<section id="top-denial-reason" data-rail="02 &middot; #1 REASON">
<h2>The #1 Denial Reason: "Lack of Maintenance"</h2>
<p>When an insurer can't find a record of regular inspection, "lack of maintenance" becomes the default explanation for wear they'd otherwise attribute to a storm. It's not that the roof wasn't maintained &mdash; it's that nobody can show that it was.</p>
<div class="risks">
<div class="risk-card reveal">
<span class="num big red">40%</span>
<h3>Insurance denials</h3>
<p>Of claims are denied for "lack of maintenance" when proof of regular inspection is missing from the file.</p>
<span class="stamp-tag danger">&#10005; HIGH RISK</span>
</div>
<div class="risk-card reveal">
<span class="num big amber">5 yrs</span>
<h3>Life reduced</h3>
<p>A minor leak left undocumented for six months can shorten a roof's serviceable life by up to five years.</p>
<span class="stamp-tag caution">&#9888; CAUTION</span>
</div>
<div class="risk-card reveal">
<span class="num big sky">15 min</span>
<h3>To build the record</h3>
<p>The time it actually takes to capture a dated, facet-mapped inspection that removes the ambiguity entirely.</p>
<span class="stamp-tag ok">&#10003; VERIFIED</span>
</div>
</div>
</section>
<section id="what-a-denial-costs" data-rail="03 &middot; TRUE COST">
<h2>What a Denial Actually Costs You</h2>
<p>A denied claim rarely just disappears. It turns into a second inspection, an appeal letter, weeks of delay while the homeowner grows frustrated with everyone involved, and in some cases a job that gets pulled entirely once trust in the process erodes. None of that shows up on the original bid, but all of it hits your calendar and your reputation.</p>
</section>
<blockquote class="pull-quote">
Insurers don't deny roofs. They deny files that can't prove what happened to them.
</blockquote>
<section id="the-fix" data-rail="04 &middot; THE FIX">
<h2>The Fix: Photograph Everything, Timestamp Everything</h2>
<p>The single biggest lever here isn't a better argument with the adjuster after the fact &mdash; it's a better file before the claim is even filed. A dated, geo-tagged, facet-by-facet inspection record does more to prevent a denial than any appeal letter written after one.</p>
<ul>
<li>Capture a full inspection at first contact, not just at claim time.</li>
<li>Store records in a format that survives a change of ownership or a change of contractor.</li>
<li>Make the record easy to hand over &mdash; carriers move faster through formats they already recognize.</li>
</ul>
</section>
<section id="pre-claim-checklist" data-rail="05 &middot; CHECKLIST">
<h2>A Quick Pre-Claim Checklist</h2>
<ol>
<li>Full-roof scan with facet-level measurements, not just a visual walkthrough.</li>
<li>Photos of every existing wear pattern, dated before the storm event if possible.</li>
<li>Shingle class, install date, and any prior repair history on file.</li>
<li>One clean export, ready to attach to the claim on day one.</li>
</ol>
</section>
</div>
<div class="founder-card terms-cta reveal">
<span class="stamp-tag ok">&#10003; CLAIM-READY BY DEFAULT</span>
<h2>Stop building the file <em>after</em> the denial.</h2>
<p>See how a phone-based scan builds a claim-ready record on day one, before there's ever a dispute to fight.</p>
<div class="cta-alt">
<a class="btn solid" href="../../index.html#book">Book a demo</a>
<a class="btn" href="../../index.html#product">See the product</a>
</div>
</div>
<div class="post-foot-nav">
<a href="../">&larr; All field notes</a>
<a href="../cost-of-paperwork/">Next: The Real Cost of Paperwork &rarr;</a>
</div>
</div>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="../../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="../../index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="../../index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="../../index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="../../index.html#hero">Instant estimate</a>
<a href="../../index.html#intel">Intelligence map</a>
<a href="../../index.html#product">For contractors</a>
<a href="../../index.html#industries">For homeowners</a>
<a href="../../index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="../../index.html#founder">About</a>
<a href="../../blog/">Blog</a>
<a href="../../index.html#chaos">Why it works</a>
<a href="../../index.html#book">Book a demo</a>
<a href="../../index.html#book">Careers</a>
<a href="../../index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="../../faq.html">FAQ</a>
<a href="../../index.html">Help center</a>
<a href="../../index.html">Security</a>
<a href="../../index.html">Privacy policy</a>
<a href="../../terms/">Terms of service</a>
<a href="../../index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="../../src/blog.js"></script>
</body>
</html>
+643
View File
@@ -0,0 +1,643 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FAQ - LynkedUp Pro | The construction CRM that does the paperwork</title>
<meta name="description" content="Answers about LynkedUp Pro: the AI contractor CRM, ProCanvas field canvassing, Owners Box, AI estimating, pricing, and the Founders License.">
<meta property="og:title" content="FAQ - LynkedUp Pro">
<meta property="og:description" content="Answers about the AI-powered contractor CRM: ProCanvas, Owners Box, AI estimating, integrations, and the Founders License.">
<meta property="og:type" content="website">
<meta property="og:image" content="./og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="FAQ - LynkedUp Pro">
<meta name="twitter:description" content="Answers about the AI-powered contractor CRM: ProCanvas, Owners Box, AI estimating, integrations, and the Founders License.">
<meta name="twitter:image" content="./og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{"@type": "Question", "name": "What is LynkedUp Pro?", "acceptedAnswer": {"@type": "Answer", "text": "LynkedUp Pro is an AI-powered contractor CRM and construction operations platform built to help roofing, restoration, and construction companies manage leads, sales, projects, crews, documents, follow-ups, invoices, and job updates from one connected system."}},
{"@type": "Question", "name": "Who is LynkedUp Pro built for?", "acceptedAnswer": {"@type": "Answer", "text": "Contractors, roofers, restoration companies, field sales teams, project managers, owners, admins, canvassers, subcontractors, and growing construction businesses that need better visibility across sales and production."}},
{"@type": "Question", "name": "What is ProCanvas?", "acceptedAnswer": {"@type": "Answer", "text": "ProCanvas is LynkedUp Pro's field canvassing system, turning door knocking and field prospecting into a mobile-first, map-based workflow with property intelligence, lead tracking, rep activity, check-ins, leaderboards, and CRM syncing."}},
{"@type": "Question", "name": "What is Owners Box?", "acceptedAnswer": {"@type": "Answer", "text": "Owners Box is LynkedUp Pro's back-office command center, helping manage HR, onboarding, team roles, vendor documents, compliance alerts, company files, accounting visibility, action alerts, and owner-level business health."}},
{"@type": "Question", "name": "Does LynkedUp Pro use AI?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. LynkedUp Pro uses AI to support automation, task guidance, project insights, proposal assistance, follow-up workflows, and upcoming evidence-based estimating and field capture tools. AI should always be reviewed by a qualified human."}},
{"@type": "Question", "name": "What is the Founders License?", "acceptedAnswer": {"@type": "Answer", "text": "The Founders License is a limited early-access offer for select companies that want to reserve access before standard pricing and public availability."}}
]
}
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="./index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="./index.html#product">Product</a>
<a href="./index.html#chaos">Why it works</a>
<a href="./index.html#intel">Intelligence</a>
<a href="./index.html#pricing">Pricing</a>
<a href="./index.html#founder">About</a>
<a href="./blog/">Blog</a>
<a href="./faq.html">FAQ</a>
</nav>
<a class="btn solid" href="./index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<nav class="rail" id="rail" aria-label="Section index">
<div class="rail-track" id="railTrack">
<div class="rail-carriage" id="railCar"><span class="rail-read" id="railRead"></span></div>
</div>
</nav>
<main id="top">
<!-- ============ FAQ INTRO ============ -->
<div class="faq-hero" id="intro">
<div class="wrap">
<span class="sheet num" aria-hidden="true">01</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 01 &middot; FAQ</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Frequently asked questions</span>
<h1>Answers, not <em>guesswork.</em></h1>
<p>Straight answers about the platform, ProCanvas, Owners Box, AI estimating, and the Founders License &mdash; pulled from real conversations with contractors. Claims marked <sup>*</sup> tie back to our <a href="#disclaimer">internal &amp; field-testing disclaimer</a>.</p>
</div>
<div class="faq-search reveal">
<input type="search" id="faqSearch" autocomplete="off" spellcheck="false" placeholder="Search questions, e.g. &ldquo;SMS&rdquo; or &ldquo;Xactimate&rdquo;" aria-label="Search FAQ">
<span class="tag faq-count" id="faqCount" aria-live="polite"></span>
</div>
<div class="faq-quicknav reveal" role="group" aria-label="Jump to category">
<a class="chip" href="#ov">Overview</a>
<a class="chip" href="#sales">Sales &amp; follow-ups</a>
<a class="chip" href="#canvas">ProCanvas</a>
<a class="chip" href="#projects">Projects &amp; portal</a>
<a class="chip" href="#ownersbox">Owners Box &amp; team</a>
<a class="chip" href="#ai">AI estimating</a>
<a class="chip" href="#reporting">Reporting &amp; integrations</a>
<a class="chip" href="#fit">Who it's for</a>
<a class="chip" href="#roadmap">Roadmap</a>
<a class="chip" href="#founders">Founders License</a>
<a class="chip" href="#about">About &amp; disclaimer</a>
</div>
</div>
</div>
<!-- ============ OVERVIEW & PLATFORM ============ -->
<section class="faq-cat" id="ov">
<div class="wrap">
<span class="sheet num" aria-hidden="true">02</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 02 &middot; OVERVIEW</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Overview &amp; platform</span>
<h2>What LynkedUp Pro <em>actually is.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What is LynkedUp Pro?<i aria-hidden="true"></i></summary>
<p>LynkedUp Pro is an AI-powered contractor CRM and construction operations platform built to help roofing, restoration, and construction companies manage leads, sales, projects, crews, documents, follow-ups, invoices, and job updates from one connected system.</p>
</details>
<details>
<summary>Who is LynkedUp Pro built for?<i aria-hidden="true"></i></summary>
<p>Contractors, roofers, restoration companies, field sales teams, project managers, owners, admins, canvassers, subcontractors, and growing construction businesses that need better visibility across sales and production.</p>
</details>
<details>
<summary>Is LynkedUp Pro just a CRM?<i aria-hidden="true"></i></summary>
<p>No. LynkedUp Pro includes CRM tools, but it is designed to operate more like a full contractor business command center. It connects sales, project management, canvassing, automation, customer communication, document storage, owner reporting, and upcoming AI-powered estimating features.</p>
</details>
<details>
<summary>What makes LynkedUp Pro different from other contractor CRMs?<i aria-hidden="true"></i></summary>
<p>LynkedUp Pro was built from real contractor workflows, not just generic software templates. The platform is designed to help contractors move faster, reduce missed follow-ups, improve job visibility, and keep sales, production, and ownership aligned from first lead to final payment.<sup>*</sup></p>
</details>
<details>
<summary>What can I manage inside LynkedUp Pro?<i aria-hidden="true"></i></summary>
<p>Leads, contacts, customers, jobs, inspections, estimates, project stages, documents, photos, invoices, payments, follow-ups, team members, subcontractors, and business health reporting, depending on your plan and enabled features.</p>
</details>
</div>
</div>
</section>
<!-- ============ SALES, FOLLOW-UPS & MESSAGING ============ -->
<section class="faq-cat" id="sales">
<div class="wrap">
<span class="sheet num" aria-hidden="true">03</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 03 &middot; SALES &amp; FOLLOW-UPS</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Sales, follow-ups &amp; messaging</span>
<h2>Never let a lead <em>go quiet.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>Does LynkedUp Pro help with follow-ups?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to automate follow-ups by text and email for leads, appointments, estimates, active jobs, completed projects, review requests, referrals, and re-engagement campaigns.<sup>*</sup></p>
</details>
<details>
<summary>Can LynkedUp Pro send SMS and email messages?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro supports customer communication through SMS and email workflows. SMS, email, usage, sending numbers, data, and related messaging services may require additional monthly charges depending on volume and configuration.</p>
</details>
<details>
<summary>Does each company need its own phone number for texting?<i aria-hidden="true"></i></summary>
<p>Most companies should use a dedicated sending number or approved messaging setup for better deliverability, branding, tracking, and compliance. LynkedUp Pro can support tenant-level communication setup so each company can manage its own customer messaging environment.</p>
</details>
<details>
<summary>Does LynkedUp Pro support automated appointment reminders?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro can help automate appointment confirmations, prep reminders, on-the-way messages, estimate follow-ups, job updates, and post-project review requests.<sup>*</sup></p>
</details>
<details>
<summary>Can LynkedUp Pro help reduce missed follow-ups?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to reduce missed follow-ups by automating reminders, customer messages, lead nurture sequences, and estimate follow-up workflows.<sup>*</sup></p>
</details>
<details>
<summary>Can LynkedUp Pro help my reps sell more?<i aria-hidden="true"></i></summary>
<p>LynkedUp Pro is designed to help reps stay organized, respond faster, follow up consistently, and manage opportunities more clearly, which may help improve sales performance when used correctly.<sup>*</sup></p>
</details>
</div>
</div>
</section>
<!-- ============ PROCANVAS ============ -->
<section class="faq-cat" id="canvas">
<div class="wrap">
<span class="sheet num" aria-hidden="true">04</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 04 &middot; PROCANVAS</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">ProCanvas &middot; field canvassing</span>
<h2>Door knocking, <em>mapped and measured.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What is ProCanvas?<i aria-hidden="true"></i></summary>
<p>ProCanvas is LynkedUp Pro's field canvassing system. It is designed to turn door knocking and field prospecting into a mobile-first, map-based workflow with property intelligence, lead tracking, rep activity, check-ins, leaderboards, and CRM syncing.<sup>*</sup></p>
</details>
<details>
<summary>How does ProCanvas help sales reps?<i aria-hidden="true"></i></summary>
<p>ProCanvas helps reps see where they are working, log activity, track knocks, set inspections, capture notes, and push qualified opportunities into the CRM. It is designed to make canvassing feel more organized, competitive, and easier to manage.<sup>*</sup></p>
</details>
<details>
<summary>Can owners see what the field team is doing?<i aria-hidden="true"></i></summary>
<p>Yes. Owners and managers can view activity, pipeline movement, job status, follow-ups, and team performance from inside the platform. This helps leadership see the health of the business without chasing updates across multiple tools.<sup>*</sup></p>
</details>
</div>
</div>
</section>
<!-- ============ PROJECTS, PORTAL & DOCUMENTS ============ -->
<section class="faq-cat" id="projects">
<div class="wrap">
<span class="sheet num" aria-hidden="true">05</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 05 &middot; PROJECTS &amp; PORTAL</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Projects, customer portal &amp; documents</span>
<h2>One job file, <em>every stakeholder.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What is the Project Command Center?<i aria-hidden="true"></i></summary>
<p>The Project Command Center is the central view for a job. It is designed to show customer details, project status, estimate information, documents, photos, payments, production updates, activity history, and risk indicators in one place.</p>
</details>
<details>
<summary>Does LynkedUp Pro include a customer portal?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro includes customer portal functionality designed to keep homeowners updated on project status, technician ETA, next steps, approvals, change order requests, messages, and project notifications.</p>
</details>
<details>
<summary>Can customers approve estimates or change orders online?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to support customer approvals, estimate reviews, change order requests, status updates, and documented communication from the customer portal.</p>
</details>
<details>
<summary>Can subcontractors upload job photos?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to allow subcontractors or field users to upload photos, document completed work, and confirm that a job is ready for the next stage of production or payment review.</p>
</details>
<details>
<summary>Can owners approve subcontractor payment releases?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to help owners or authorized managers review job completion evidence before releasing subcontractor payments or commission-related approvals.</p>
</details>
<details>
<summary>Can LynkedUp Pro help project managers run jobs better?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to help project managers track project stages, job photos, customer updates, crew activity, documents, schedules, production notes, and next steps in one place.<sup>*</sup></p>
</details>
</div>
</div>
</section>
<!-- ============ OWNERS BOX, TEAM & ROLES ============ -->
<section class="faq-cat" id="ownersbox">
<div class="wrap">
<span class="sheet num" aria-hidden="true">06</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 06 &middot; OWNERS BOX</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Owners Box, team &amp; roles</span>
<h2>The back office, <em>run from one screen.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What is Owners Box?<i aria-hidden="true"></i></summary>
<p>Owners Box is LynkedUp Pro's back-office command center. It is designed to help manage HR, onboarding, team roles, vendor documents, compliance alerts, company files, accounting visibility, action alerts, and owner-level business health.</p>
</details>
<details>
<summary>Can LynkedUp Pro help with onboarding new team members?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to make onboarding easier by allowing admins to invite new users, collect required information, classify team roles, manage documents, and track onboarding status.<sup>*</sup></p>
</details>
<details>
<summary>What roles can be assigned inside LynkedUp Pro?<i aria-hidden="true"></i></summary>
<p>Common roles include Owner, Admin, Sales Rep, Project Manager, Site Supervisor, Canvasser, Subcontractor, Sales Manager, Team Lead, and Other. Smaller companies can also assign multiple responsibilities to the same person.</p>
</details>
<details>
<summary>Can LynkedUp Pro help owners understand business health?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to give owners better visibility into active jobs, follow-ups, payments, production status, team activity, compliance items, and business alerts.<sup>*</sup></p>
</details>
</div>
</div>
</section>
<!-- ============ AI ESTIMATING ============ -->
<section class="faq-cat" id="ai">
<div class="wrap">
<span class="sheet num" aria-hidden="true">07</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 07 &middot; AI ESTIMATING</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">AI estimating</span>
<h2>AI-assisted, <em>human-reviewed.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>Can LynkedUp Pro help with roofing estimates?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro includes current and upcoming estimating workflows designed for retail roofing, storm restoration, insurance-style scopes, Good / Better / Best options, proposal generation, and homeowner-friendly explanations.<sup>*</sup></p>
</details>
<details>
<summary>Does LynkedUp Pro replace Xactimate?<i aria-hidden="true"></i></summary>
<p>LynkedUp Pro is not positioned as a direct replacement for every Xactimate use case. It is designed to help contractors create structured, evidence-supported estimating workflows and proposal outputs. Insurance-grade and courtroom-ready estimating features may depend on enabled modules, data quality, and workflow setup.</p>
</details>
<details>
<summary>Does LynkedUp Pro use AI?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro uses AI to support automation, task guidance, project insights, proposal assistance, follow-up workflows, and upcoming evidence-based estimating and field capture tools.</p>
</details>
<details>
<summary>Is the AI always accurate?<i aria-hidden="true"></i></summary>
<p>No. AI should always be reviewed by a qualified human. LynkedUp Pro is designed to assist contractors, not replace professional judgment, code compliance review, carrier requirements, or final business decisions.</p>
</details>
</div>
</div>
</section>
<!-- ============ REPORTING, CUSTOMIZATION & INTEGRATIONS ============ -->
<section class="faq-cat" id="reporting">
<div class="wrap">
<span class="sheet num" aria-hidden="true">08</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 08 &middot; REPORTING &amp; INTEGRATIONS</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Reporting, customization &amp; integrations</span>
<h2>Fits the way <em>you already run jobs.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>Does LynkedUp Pro include reporting?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to include reporting and dashboard views for leads, sales activity, project status, follow-ups, production, payments, and owner-level business insights.</p>
</details>
<details>
<summary>Can LynkedUp Pro be customized?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is built to be customizable. Contractors can use the platform out of the box or discuss custom workflows, automations, dashboards, forms, integrations, and business-specific modifications.</p>
</details>
<details>
<summary>Can LynkedUp Pro be customized for my company's process?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro can be configured around your sales process, production stages, team structure, lead sources, automation sequences, customer communication style, and reporting needs.</p>
</details>
<details>
<summary>Does LynkedUp Pro integrate with other tools?<i aria-hidden="true"></i></summary>
<p>LynkedUp Pro is designed to support integrations with common business systems such as calendars, communication tools, accounting platforms, payment systems, proposal workflows, and data providers depending on the configuration and development roadmap.</p>
</details>
<details>
<summary>Does LynkedUp Pro support payments and invoices?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to support invoice and payment workflows. Payment processor fees, third-party service fees, and advanced payment automations may be billed separately.</p>
</details>
</div>
</div>
</section>
<!-- ============ WHO IT'S FOR & MOBILE ============ -->
<section class="faq-cat" id="fit">
<div class="wrap">
<span class="sheet num" aria-hidden="true">09</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 09 &middot; WHO IT'S FOR</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Who it's for &amp; mobile</span>
<h2>From one truck <em>to a whole crew.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>Is LynkedUp Pro mobile-friendly?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed for contractors who work from the field, office, truck, jobsite, and home. Key workflows are designed to be mobile-first or mobile-friendly.</p>
</details>
<details>
<summary>Does LynkedUp Pro work for companies with small teams?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed for both smaller contractors where people wear multiple hats and growing companies that need more structure across sales, production, admin, and ownership.</p>
</details>
<details>
<summary>Does LynkedUp Pro work for larger construction teams?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is designed to support teams with multiple reps, project managers, admins, subcontractors, managers, and owners who need role-based visibility and workflow control.</p>
</details>
<details>
<summary>Is LynkedUp Pro only for roofing companies?<i aria-hidden="true"></i></summary>
<p>No. Roofing is a major focus, but LynkedUp Pro is designed for construction, restoration, exterior remodeling, field sales, trade contractors, and service-based construction businesses.</p>
</details>
</div>
</div>
</section>
<!-- ============ ROADMAP & AVAILABILITY ============ -->
<section class="faq-cat" id="roadmap">
<div class="wrap">
<span class="sheet num" aria-hidden="true">10</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 10 &middot; ROADMAP</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Roadmap &amp; availability</span>
<h2>What's shipped, <em>what's next.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What upcoming features are planned?<i aria-hidden="true"></i></summary>
<p>Upcoming and advanced features may include enhanced AI estimating, retail proposal tools, insurance-grade estimate support, LiDAR and digital twin workflows, advanced canvassing intelligence, expanded automation, customer portal improvements, owner reporting, and deeper back-office controls.</p>
</details>
<details>
<summary>Are all features available today?<i aria-hidden="true"></i></summary>
<p>Not every feature may be available for every user, plan, or stage of rollout. Some features are active, some are in beta, and some are part of the upcoming roadmap. Availability may vary by subscription, license, configuration, and development phase.</p>
</details>
</div>
</div>
</section>
<!-- ============ FOUNDERS LICENSE & GETTING STARTED ============ -->
<section class="faq-cat" id="founders">
<div class="wrap">
<span class="sheet num" aria-hidden="true">11</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 11 &middot; FOUNDERS LICENSE</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Founders License &amp; getting started</span>
<h2>Reserve access <em>before it's public.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>What is the Founders License?<i aria-hidden="true"></i></summary>
<p>The Founders License is a limited early-access offer for select companies that want to reserve access before standard pricing and public availability. Per-seat Starter and Pro plans are currently on hold while this offer is live. Founders License terms, limits, included features, and usage costs should be reviewed before purchase.</p>
</details>
<details>
<summary>How much does the Founders Lifetime License cost?<i aria-hidden="true"></i></summary>
<p>$2,000, one-time. It reserves lifetime access to the core platform under the Founders License terms while per-seat pricing is on hold. SMS, email, data usage, GPU usage, advanced AI features, Campaign-X, LiDAR scans, digital twin technology, custom development, and third-party services may still incur additional monthly or usage-based charges.</p>
</details>
<details>
<summary>Does the Founders License include everything forever?<i aria-hidden="true"></i></summary>
<p>No. The Founders License includes access to core platform features based on the license terms. SMS, email, data usage, GPU usage, advanced AI features, Campaign-X, LiDAR scans, digital twin technology, custom development, and third-party services may incur additional monthly or usage-based charges.</p>
</details>
<details>
<summary>Why should I book a demo?<i aria-hidden="true"></i></summary>
<p>A demo gives you a direct look at the LynkedUp Pro platform, current features, upcoming roadmap, and how the system can fit your company's sales, production, and owner workflows.</p>
</details>
<details>
<summary>How long is the demo?<i aria-hidden="true"></i></summary>
<p>The standard demo is 30 minutes and gives you a walkthrough of the LynkedUp Pro platform and upcoming features. A 1-hour custom buildout meeting is available for companies that want to discuss deeper customization or a business-specific solution.</p>
</details>
<details>
<summary>How do I reserve my spot?<i aria-hidden="true"></i></summary>
<p>You can reserve your spot by booking a demo or requesting access before the Founders License offer is gone. Availability may be limited by company, license count, rollout phase, or onboarding capacity.</p>
</details>
</div>
</div>
</section>
<!-- ============ ABOUT & DISCLAIMER ============ -->
<section class="faq-cat" id="about">
<div class="wrap">
<span class="sheet num" aria-hidden="true">12</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 12 &middot; ABOUT</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">About LynkedUp Pro</span>
<h2>Built by contractors, <em>tested in the field.</em></h2>
</div>
<div class="faq reveal">
<details>
<summary>Is LynkedUp Pro a new company?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro is a new and growing company. Some claims, results, projections, workflows, and performance statements are based on internal testing and field testing with founding users.<sup>*</sup></p>
</details>
<details>
<summary>Why are some claims marked with an asterisk?<i aria-hidden="true"></i></summary>
<p>Claims marked with an asterisk are based on internal testing, field testing, early workflows, founding-user feedback, projected use cases, or controlled product testing. Results may vary by company, team size, setup, data quality, adoption, market, and usage.</p>
</details>
<details>
<summary>Is LynkedUp Pro built by contractors?<i aria-hidden="true"></i></summary>
<p>Yes. LynkedUp Pro was built from real contractor experience and is designed around the daily problems contractors face in sales, production, communication, follow-up, documentation, and owner visibility.</p>
</details>
</div>
<div class="faq-note reveal" id="disclaimer">
<span class="tag">Website disclaimer for asterisk claims<sup>*</sup></span>
<p>Claims marked with an asterisk are based on a mix of internal testing, field testing, early product workflows, founding-user feedback, and projected use cases. LynkedUp Pro is a new and growing company curated by a group of construction industry veterans and some of the brightest minds in technology and AI. Results may vary by business size, team adoption, data quality, configuration, market conditions, usage volume, and enabled features. Some user names, company names, or examples may be changed to protect customer privacy and trade secrets.</p>
</div>
</div>
</section>
<!-- ============ STILL HAVE QUESTIONS · CTA ============ -->
<section id="cta">
<div class="wrap">
<span class="sheet num" aria-hidden="true">13</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 13 &middot; BOOK</span><i class="r"></i></div>
<div class="founder-card faq-cta reveal">
<span class="stamp-tag ok">&#10003; STILL HAVE QUESTIONS?</span>
<h2>Bring your <em>real questions.</em></h2>
<p>Get a 30-minute walkthrough of the platform, or a 1-hour buildout call if your workflow needs custom work.</p>
<div class="cta-alt">
<a class="btn solid" href="./index.html#book">Book a demo</a>
<a class="btn" href="tel:8662596533">Call (866) 259-6533</a>
</div>
</div>
</div>
</section>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="./index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="./index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="./index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="./index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="./index.html#hero">Instant estimate</a>
<a href="./index.html#intel">Intelligence map</a>
<a href="./index.html#product">For contractors</a>
<a href="./index.html#industries">For homeowners</a>
<a href="./index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="./index.html#founder">About</a>
<a href="./blog/">Blog</a>
<a href="./index.html#chaos">Why it works</a>
<a href="./index.html#book">Book a demo</a>
<a href="./index.html#book">Careers</a>
<a href="./index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="./faq.html">FAQ</a>
<a href="./index.html">Help center</a>
<a href="./index.html">Security</a>
<a href="./index.html">Privacy policy</a>
<a href="./terms/">Terms of service</a>
<a href="./index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="./src/faq.js"></script>
</body>
</html>
+1315
View File
File diff suppressed because it is too large Load Diff
+1083
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "lynkedup-fusion",
"private": true,
"version": "1.0.0",
"author": "Sarthak",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/inter": "^5.0.18",
"@fontsource/jetbrains-mono": "^5.0.20",
"gsap": "^3.12.5",
"three": "^0.165.0"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"vite": "^5.3.1"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

+172
View File
@@ -0,0 +1,172 @@
import "@fontsource-variable/inter";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "./styles.css";
console.log("%cCreated by Sarthak", "font-size:14px;font-weight:700;color:#F2A83B;");
(function(){
var reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
var finePointer = matchMedia("(pointer: fine)").matches;
/* ---------- scrollbar materialises only while scrolling ---------- */
(function(){
var to = null;
addEventListener("scroll", function(){
document.documentElement.classList.add("scrolling");
clearTimeout(to);
to = setTimeout(function(){ document.documentElement.classList.remove("scrolling"); }, 900);
}, { passive: true, capture: true });
})();
/* ---------- mobile scroll progress bar ---------- */
(function(){
var bar = document.getElementById("mprogBar");
if(!bar) return;
var raf = null;
function upd(){
raf = null;
var doc = document.scrollingElement || document.documentElement;
var max = Math.max(1, doc.scrollHeight - innerHeight);
bar.style.transform = "scaleX(" + Math.min(1, window.scrollY / max).toFixed(4) + ")";
}
addEventListener("scroll", function(){ if(raf == null) raf = requestAnimationFrame(upd); }, { passive: true });
upd();
})();
/* ---------- mobile nav ---------- */
(function(){
var mb = document.getElementById("menuBtn");
if(!mb) return;
mb.addEventListener("click", function(){
var open = document.body.classList.toggle("nav-open");
mb.setAttribute("aria-expanded", open ? "true" : "false");
});
[].slice.call(document.querySelectorAll(".nav-links a")).forEach(function(a){
a.addEventListener("click", function(){
document.body.classList.remove("nav-open");
mb.setAttribute("aria-expanded", "false");
});
});
})();
/* ---------- footer: hail alerts by ZIP ---------- */
(function(){
var form = document.getElementById("zipForm");
if(!form) return;
var zin = document.getElementById("zipInput");
form.addEventListener("submit", function(e){
e.preventDefault();
var v = zin.value.trim();
if(!/^\d{5}$/.test(v)){ zin.focus(); zin.select(); return; }
form.innerHTML = "<div class='fa-done'><span class='tag'>&#9873; WATCHING " + v + "</span>" +
"<span>First alert lands after the next radar-verified swath. No spam, only storms.</span></div>";
});
})();
/* ---------- station rail: generic, built from any [data-rail] section on the page ---------- */
(function(){
var rail = document.getElementById("rail"), track = document.getElementById("railTrack");
if(!rail || !track) return;
var car = document.getElementById("railCar"), read = document.getElementById("railRead");
var secs = [].slice.call(document.querySelectorAll("[data-rail]")).map(function(el){
return { el: el, label: el.getAttribute("data-rail") };
});
if(!secs.length) return;
var nodes = secs.map(function(s){
var b = document.createElement("button");
b.className = "rail-node"; b.type = "button"; b.title = s.label;
b.setAttribute("aria-label", s.label);
b.addEventListener("click", function(){
scrollTo({top:s.el.offsetTop - 52, behavior:reduced ? "auto" : "smooth"});
});
track.appendChild(b);
return b;
});
var tops = [], maxScroll = 1;
function layout(){
var doc = document.scrollingElement || document.documentElement;
maxScroll = Math.max(1, doc.scrollHeight - innerHeight);
tops = secs.map(function(s){ return Math.max(0, Math.min(s.el.offsetTop - 52, maxScroll)); });
tops.forEach(function(tp, i){ nodes[i].style.top = (tp / maxScroll * 100) + "%"; });
}
var carP = 0, target = 0, actLabel = "", raf = null, lastT = 0;
function frame(now){
var rawDt = lastT ? now - lastT : 16;
var dt = Math.min(rawDt, 50);
lastT = now;
var k = rawDt > 400 ? 1 : 1 - Math.exp(-dt / 110);
carP += (target - carP) * k;
if(Math.abs(target - carP) < .0005) carP = target;
car.style.top = (carP * 100) + "%";
read.textContent = actLabel + " · " + Math.round(carP * 100) + "%";
if(carP === target){ raf = null; lastT = 0; return; }
raf = requestAnimationFrame(frame);
}
function upd(){
var doc = document.scrollingElement || document.documentElement;
if(Math.abs(doc.scrollHeight - innerHeight - maxScroll) > 2) layout();
var y = window.scrollY;
target = Math.max(0, Math.min(1, y / maxScroll));
var act = 0;
tops.forEach(function(tp, i){ if(y >= tp - innerHeight * .35) act = i; });
if(maxScroll - y < 4) act = secs.length - 1;
nodes.forEach(function(n, i){ n.classList.toggle("on", i === act); });
actLabel = secs[act].label;
if(raf == null){ lastT = 0; raf = requestAnimationFrame(frame); }
}
addEventListener("scroll", upd, {passive:true});
addEventListener("resize", function(){ layout(); upd(); });
addEventListener("load", function(){ layout(); upd(); });
layout(); upd();
})();
/* ---------- scroll reveals: 150ms, 60ms stagger ---------- */
var revealEls = [].slice.call(document.querySelectorAll(".reveal"));
if(!reduced && "IntersectionObserver" in window){
var groups = new Map();
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if(!e.isIntersecting && e.boundingClientRect.top >= 0) return;
io.unobserve(e.target);
var parent = e.target.parentElement;
var idx = groups.get(parent) || 0;
groups.set(parent, idx + 1);
setTimeout(function(){ e.target.classList.add("in"); }, idx * 60);
setTimeout(function(){ groups.set(parent, Math.max(0,(groups.get(parent)||1)-1)); }, 400);
});
}, {threshold:.15});
revealEls.forEach(function(el){ io.observe(el); });
} else {
revealEls.forEach(function(el){ el.classList.add("in"); });
}
/* ---------- card tilt physics (spring, with glare tracking) ---------- */
if(finePointer && !reduced){
[].slice.call(document.querySelectorAll(".risk-card,.founder-card,.feat,.terms-card")).forEach(function(el){
var rx=0,ry=0,tx=0,ty=0,vx=0,vy=0,raf=null,over=false;
function frame(){
vx+=(tx-rx)*.16; vy+=(ty-ry)*.16; vx*=.72; vy*=.72;
rx+=vx; ry+=vy;
if(!over && Math.abs(rx)<.02 && Math.abs(ry)<.02 && Math.abs(vx)<.02 && Math.abs(vy)<.02){
el.style.transform=""; raf=null; return;
}
el.style.transform="perspective(900px) rotateX("+rx.toFixed(2)+"deg) rotateY("+ry.toFixed(2)+"deg) translateY(-2px)";
raf=requestAnimationFrame(frame);
}
el.addEventListener("pointerenter",function(){
over=true; el.classList.add("tilting");
if(!raf) raf=requestAnimationFrame(frame);
});
el.addEventListener("pointermove",function(e){
var r=el.getBoundingClientRect();
var px=(e.clientX-r.left)/r.width, py=(e.clientY-r.top)/r.height;
ty=(px-.5)*5; tx=(.5-py)*5;
el.style.setProperty("--gx",(px*100)+"%");
el.style.setProperty("--gy",(py*100)+"%");
});
el.addEventListener("pointerleave",function(){ over=false; tx=0; ty=0; });
});
}
})();
+180
View File
@@ -0,0 +1,180 @@
import "@fontsource-variable/inter";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "./styles.css";
console.log("%cCreated by Sarthak", "font-size:14px;font-weight:700;color:#F2A83B;");
(function(){
var reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- scrollbar materialises only while scrolling ---------- */
(function(){
var to = null;
addEventListener("scroll", function(){
document.documentElement.classList.add("scrolling");
clearTimeout(to);
to = setTimeout(function(){ document.documentElement.classList.remove("scrolling"); }, 900);
}, { passive: true, capture: true });
})();
/* ---------- mobile scroll progress bar ---------- */
(function(){
var bar = document.getElementById("mprogBar");
if(!bar) return;
var raf = null;
function upd(){
raf = null;
var doc = document.scrollingElement || document.documentElement;
var max = Math.max(1, doc.scrollHeight - innerHeight);
bar.style.transform = "scaleX(" + Math.min(1, window.scrollY / max).toFixed(4) + ")";
}
addEventListener("scroll", function(){ if(raf == null) raf = requestAnimationFrame(upd); }, { passive: true });
upd();
})();
/* ---------- mobile nav ---------- */
(function(){
var mb = document.getElementById("menuBtn");
if(!mb) return;
mb.addEventListener("click", function(){
var open = document.body.classList.toggle("nav-open");
mb.setAttribute("aria-expanded", open ? "true" : "false");
});
[].slice.call(document.querySelectorAll(".nav-links a")).forEach(function(a){
a.addEventListener("click", function(){
document.body.classList.remove("nav-open");
mb.setAttribute("aria-expanded", "false");
});
});
})();
/* ---------- footer: hail alerts by ZIP ---------- */
(function(){
var form = document.getElementById("zipForm");
if(!form) return;
var zin = document.getElementById("zipInput");
form.addEventListener("submit", function(e){
e.preventDefault();
var v = zin.value.trim();
if(!/^\d{5}$/.test(v)){ zin.focus(); zin.select(); return; }
form.innerHTML = "<div class='fa-done'><span class='tag'>&#9873; WATCHING " + v + "</span>" +
"<span>First alert lands after the next radar-verified swath. No spam, only storms.</span></div>";
});
})();
/* ---------- station rail: section index ---------- */
(function(){
var rail = document.getElementById("rail"), track = document.getElementById("railTrack");
if(!rail || !track) return;
var car = document.getElementById("railCar"), read = document.getElementById("railRead");
var defs = [
["intro","01 · FAQ"], ["ov","02 · OVERVIEW"], ["sales","03 · SALES"], ["canvas","04 · PROCANVAS"],
["projects","05 · PROJECTS"], ["ownersbox","06 · OWNERS BOX"], ["ai","07 · AI"],
["reporting","08 · REPORTING"], ["fit","09 · FIT"], ["roadmap","10 · ROADMAP"],
["founders","11 · FOUNDERS"], ["about","12 · ABOUT"], ["cta","13 · BOOK"]
];
var secs = defs.map(function(d){ return {el:document.getElementById(d[0]), label:d[1]}; })
.filter(function(s){ return s.el; });
var nodes = secs.map(function(s){
var b = document.createElement("button");
b.className = "rail-node"; b.type = "button"; b.title = s.label;
b.setAttribute("aria-label", s.label);
b.addEventListener("click", function(){
scrollTo({top:s.el.offsetTop - 52, behavior:reduced ? "auto" : "smooth"});
});
track.appendChild(b);
return b;
});
var tops = [], maxScroll = 1;
function layout(){
var doc = document.scrollingElement || document.documentElement;
maxScroll = Math.max(1, doc.scrollHeight - innerHeight);
/* nodes live in the SAME space as the carriage: clamped scroll position / max scroll */
tops = secs.map(function(s){ return Math.max(0, Math.min(s.el.offsetTop - 52, maxScroll)); });
tops.forEach(function(tp, i){ nodes[i].style.top = (tp / maxScroll * 100) + "%"; });
}
var carP = 0, target = 0, actLabel = "", raf = null, lastT = 0;
function frame(now){
var rawDt = lastT ? now - lastT : 16;
var dt = Math.min(rawDt, 50);
lastT = now;
var k = rawDt > 400 ? 1 : 1 - Math.exp(-dt / 110);
carP += (target - carP) * k;
if(Math.abs(target - carP) < .0005) carP = target;
car.style.top = (carP * 100) + "%";
read.textContent = actLabel + " · " + Math.round(carP * 100) + "%";
if(carP === target){ raf = null; lastT = 0; return; }
raf = requestAnimationFrame(frame);
}
function upd(){
var doc = document.scrollingElement || document.documentElement;
if(Math.abs(doc.scrollHeight - innerHeight - maxScroll) > 2) layout();
var y = window.scrollY;
target = Math.max(0, Math.min(1, y / maxScroll));
var act = 0;
tops.forEach(function(tp, i){ if(y >= tp - innerHeight * .35) act = i; });
if(maxScroll - y < 4) act = secs.length - 1;
nodes.forEach(function(n, i){ n.classList.toggle("on", i === act); });
actLabel = secs[act].label;
if(raf == null){ lastT = 0; raf = requestAnimationFrame(frame); }
}
addEventListener("scroll", upd, {passive:true});
addEventListener("resize", function(){ layout(); upd(); });
addEventListener("load", function(){ layout(); upd(); });
layout(); upd();
})();
/* ---------- scroll reveals: 150ms, 60ms stagger ---------- */
var revealEls = [].slice.call(document.querySelectorAll(".reveal"));
if(!reduced && "IntersectionObserver" in window){
var groups = new Map();
var io = new IntersectionObserver(function(entries){
entries.forEach(function(e){
if(!e.isIntersecting && e.boundingClientRect.top >= 0) return;
io.unobserve(e.target);
var parent = e.target.parentElement;
var idx = groups.get(parent) || 0;
groups.set(parent, idx + 1);
setTimeout(function(){ e.target.classList.add("in"); }, idx * 60);
setTimeout(function(){ groups.set(parent, Math.max(0,(groups.get(parent)||1)-1)); }, 400);
});
}, {threshold:.15});
revealEls.forEach(function(el){ io.observe(el); });
} else {
revealEls.forEach(function(el){ el.classList.add("in"); });
}
/* ---------- FAQ search filter ---------- */
(function(){
var input = document.getElementById("faqSearch");
if(!input) return;
var count = document.getElementById("faqCount");
var groups = [].slice.call(document.querySelectorAll(".faq-cat")).map(function(cat){
return {
cat: cat,
rows: [].slice.call(cat.querySelectorAll(".faq details")).map(function(d){
return { el: d, text: d.textContent.toLowerCase() };
})
};
});
function run(){
var q = input.value.trim().toLowerCase();
var shown = 0;
groups.forEach(function(group){
var visible = 0;
group.rows.forEach(function(row){
var match = !q || row.text.indexOf(q) !== -1;
row.el.classList.toggle("hide", !match);
if(match) visible++;
});
group.cat.classList.toggle("hide", visible === 0);
shown += visible;
});
if(count) count.textContent = q ? shown + " match" + (shown === 1 ? "" : "es") + " for “" + input.value.trim() + "”" : "";
dispatchEvent(new Event("resize"));
}
input.addEventListener("input", run);
})();
})();
+1459
View File
File diff suppressed because it is too large Load Diff
+703
View File
@@ -0,0 +1,703 @@
import * as THREE from "three";
const v = (x, y, z) => new THREE.Vector3(x, y, z);
function mk(geo, mat) {
const m = new THREE.Mesh(geo, mat);
m.castShadow = true;
m.receiveShadow = true;
return m;
}
function box(w, h, d, mat, x = 0, y = 0, z = 0) {
const m = mk(new THREE.BoxGeometry(w, h, d), mat);
m.position.set(x, y, z);
return m;
}
function quadGeo(a, b, c, d) {
const g = new THREE.BufferGeometry();
g.setFromPoints([a, b, c, a, c, d]);
g.computeVertexNormals();
const uv = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
g.setAttribute("uv", new THREE.BufferAttribute(uv, 2));
return g;
}
function triGeo(a, b, c) {
const g = new THREE.BufferGeometry();
g.setFromPoints([a, b, c]);
g.computeVertexNormals();
g.setAttribute("uv", new THREE.BufferAttribute(new Float32Array([0, 0, 1, 0, 0.5, 1]), 2));
return g;
}
/* thin cap box laid along segment a->b (ridge caps, hip caps, highlights) */
function capAlong(a, b, mat, h = 0.55, w = 1.35) {
const len = a.distanceTo(b);
const m = box(len, h, w, mat);
m.position.copy(a).add(b).multiplyScalar(0.5);
const dir = v(0, 0, 0).subVectors(b, a).normalize();
m.quaternion.setFromUnitVectors(v(1, 0, 0), dir);
return m;
}
/* ---------------- roof builders (geometry == measurement source) ------------- */
function gableRoof({ span, len, pitch, oE = 1.5, oG = 1.2, y0, matFront, matBack, capMat, fasciaMat }) {
const p = pitch / 12, th = 0.45;
const theta = Math.atan(p);
const rise = (span / 2) * p;
const ridgeY = y0 + rise;
const halfH = span / 2 + oE;
const slopeLen = halfH / Math.cos(theta);
const eaveY = ridgeY - halfH * p;
const g = new THREE.Group();
const geo = new THREE.BoxGeometry(len + 2 * oG, th, slopeLen);
for (const s of [1, -1]) {
const m = mk(geo, s === 1 ? matFront : matBack);
m.rotation.x = s * theta;
m.position.set(0, (ridgeY + eaveY) / 2 + th * 0.5, (s * halfH) / 2);
g.add(m);
}
g.add(box(len + 2 * oG + 0.3, 0.5, 1.5, capMat, 0, ridgeY + 0.45, 0));
for (const s of [1, -1]) g.add(box(len + 2 * oG, 0.85, 0.4, fasciaMat, 0, eaveY, s * halfH));
const hl = len / 2 + oG;
return {
g,
area: 2 * (len + 2 * oG) * slopeLen,
capLf: len,
ridgeY, eaveY,
ridgeSeg: [v(-hl, ridgeY + 0.5, 0), v(hl, ridgeY + 0.5, 0)],
eaveSegs: [
[v(-hl, eaveY, halfH), v(hl, eaveY, halfH)],
[v(-hl, eaveY, -halfH), v(hl, eaveY, -halfH)],
],
};
}
function hipRoof({ span, len, pitch, oE = 1.5, y0, mat, capMat, fasciaMat }) {
const p = pitch / 12;
const rise = (span / 2) * p;
const ridgeY = y0 + rise;
const eaveY = y0 - oE * p;
const dY = ridgeY - eaveY;
const halfW = span / 2 + oE, halfL = len / 2 + oE;
const rl = Math.max(0, len - span) / 2;
const R1 = v(-rl, ridgeY, 0), R2 = v(rl, ridgeY, 0);
const E = [v(-halfL, eaveY, halfW), v(halfL, eaveY, halfW), v(halfL, eaveY, -halfW), v(-halfL, eaveY, -halfW)];
const g = new THREE.Group();
g.add(mk(quadGeo(E[0], E[1], R2, R1), mat));
g.add(mk(quadGeo(E[2], E[3], R1, R2), mat));
g.add(mk(triGeo(E[1], E[2], R2), mat));
g.add(mk(triGeo(E[3], E[0], R1), mat));
for (const s of [1, -1]) g.add(box(2 * halfL, 0.85, 0.4, fasciaMat, 0, eaveY, s * halfW));
for (const s of [1, -1]) {
const f = box(2 * halfW, 0.85, 0.4, fasciaMat, s * halfL, eaveY, 0);
f.rotation.y = Math.PI / 2;
g.add(f);
}
const hipSegs = [[E[0], R1], [E[3], R1], [E[1], R2], [E[2], R2]];
hipSegs.forEach((s) => g.add(capAlong(s[0].clone().add(v(0, 0.35, 0)), s[1].clone().add(v(0, 0.35, 0)), capMat)));
if (rl > 0.1) g.add(box(rl * 2 + 0.4, 0.5, 1.5, capMat, 0, ridgeY + 0.4, 0));
const slopeLen = Math.hypot(halfW, dY);
const endSlope = Math.hypot(halfL - rl, dY);
const area = 2 * ((2 * halfL + 2 * rl) / 2) * slopeLen + 2 * 0.5 * (2 * halfW) * endSlope;
const capLf = rl * 2 + hipSegs.reduce((a, s) => a + s[0].distanceTo(s[1]), 0);
return { g, area, capLf, ridgeY, eaveY, ridgeSeg: [R1.clone().add(v(0, 0.6, 0)), R2.clone().add(v(0, 0.6, 0))], eaveSegs: [[E[0], E[1]], [E[3], E[2]]], hipSegs };
}
/* ---------------- kit parts ------------------------------------------------ */
function windowUnit(M, w = 3.2, h = 4.4) {
const g = new THREE.Group();
g.add(box(w + 0.55, h + 0.55, 0.32, M.trim));
const gl = box(w, h, 0.16, M.glass, 0, 0, 0.12);
g.add(gl);
g.add(box(w, 0.14, 0.1, M.trim, 0, 0, 0.22));
g.add(box(0.14, h, 0.1, M.trim, 0, 0, 0.22));
return g;
}
function doorUnit(M, mat, w = 3.1, h = 6.9) {
const g = new THREE.Group();
g.add(box(w + 0.6, h + 0.4, 0.3, M.trim, 0, 0.1, 0));
g.add(box(w, h, 0.24, mat, 0, 0, 0.12));
g.add(box(0.5, 0.12, 0.34, M.trim, w / 2 - 0.5, 0, 0.14));
return g;
}
function put(parent, obj, x, y, z, rotY = 0) {
obj.position.set(x, y, z);
obj.rotation.y = rotY;
parent.add(obj);
return obj;
}
function chimney(M, h, x, z, w = 3.4, d = 3) {
const g = new THREE.Group();
g.add(box(w, h, d, M.brick, 0, h / 2, 0));
g.add(box(w + 0.8, 0.6, d + 0.8, M.concrete, 0, h + 0.3, 0));
g.add(box(1.3, 1, 1.1, M.porchRoof, 0, h + 1, 0));
g.position.set(x, 0, z);
return g;
}
function tree(M, s = 1) {
const g = new THREE.Group();
const trunk = mk(new THREE.CylinderGeometry(0.45 * s, 0.7 * s, 7 * s, 7), M.trunk);
trunk.position.y = 3.5 * s;
g.add(trunk);
[[0, 9.6, 0, 4.3], [2.1, 7.8, 1.1, 3], [-1.9, 8.2, -0.8, 3.2]].forEach((f, i) => {
const fol = mk(new THREE.IcosahedronGeometry(f[3] * s, 1), i === 1 ? M.foliage2 : M.foliage);
fol.position.set(f[0] * s, f[1] * s, f[2] * s);
g.add(fol);
});
return g;
}
function shrub(M, s = 1) {
const m = mk(new THREE.IcosahedronGeometry(1.15 * s, 1), M.foliage2);
m.scale.y = 0.75;
m.position.y = 0.85 * s;
return m;
}
function mailbox(M) {
const g = new THREE.Group();
g.userData.ink = "site";
g.add(box(0.28, 3.6, 0.28, M.trunk, 0, 1.8, 0));
g.add(box(1.5, 0.9, 0.8, M.porchRoof, 0.3, 3.9, 0));
return g;
}
function pediment(M, mat, span, rise, x, y0, alongZ = false) {
const a = alongZ ? v(x, y0, -span / 2) : v(-span / 2, y0, x);
const b = alongZ ? v(x, y0, span / 2) : v(span / 2, y0, x);
const c = alongZ ? v(x, y0 + rise, 0) : v(0, y0 + rise, x);
const mm = mk(triGeo(a, b, c), mat);
mm.material = mm.material.clone();
mm.material.side = THREE.DoubleSide;
return mm;
}
function hlFromSegs(M, segs, w = 1.7) {
const g = new THREE.Group();
segs.forEach((s) => g.add(capAlong(s[0].clone().add(v(0, 0.35, 0)), s[1].clone().add(v(0, 0.35, 0)), M.hlAmber, 0.7, w)));
g.visible = false;
return g;
}
/* translucent wash over the main roof slopes, so the "roof" line-item hover reads
as prominently as the ridge/eave bar highlights instead of just a thin edge tint */
function hlRoofWash(M, roof, hip) {
const lift = v(0, 0.12, 0);
const up = (p) => p.clone().add(lift);
const R = roof.ridgeSeg, E = roof.eaveSegs;
const faces = [
quadGeo(up(E[0][0]), up(E[0][1]), up(R[1]), up(R[0])),
quadGeo(up(E[1][1]), up(E[1][0]), up(R[0]), up(R[1])),
];
if (hip) {
faces.push(triGeo(up(E[0][1]), up(E[1][1]), up(R[1])));
faces.push(triGeo(up(E[1][0]), up(E[0][0]), up(R[0])));
}
const g = new THREE.Group();
faces.forEach((geo) => {
const m = new THREE.Mesh(geo, M.hlFace);
m.castShadow = false;
m.receiveShadow = false;
g.add(m);
});
g.visible = false;
g.userData.ink = "skip";
return g;
}
/* ---------------- the three houses ---------------------------------------- */
export function buildGable(M) {
const g = new THREE.Group();
const MW = 42, MD = 28, WH = 19, PITCH = 6;
const rise = (MD / 2) * (PITCH / 12);
const roofF = M.shingle.clone(), roofB = M.shingle.clone();
const roofG1 = M.shingle.clone(), roofG2 = M.shingle.clone();
g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0)); /* foundation plinth */
g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
g.add(roof.g);
const roofWash = hlRoofWash(M, roof, false);
g.add(roofWash);
/* attached garage, gable facing the street */
const GW = 20, GD = 22, GH = 10;
const gx = MW / 2 + GW / 2 - 0.2, gz = MD / 2 - GD / 2;
g.add(box(GW + 0.8, 1.2, GD + 0.8, M.concrete, gx, 0.6, gz));
g.add(box(GW, GH, GD, M.sidingWhite, gx, GH / 2 + 1, gz));
const grise = (GW / 2) * (PITCH / 12);
const gRoofGrp = new THREE.Group();
const gRoof = gableRoof({ span: GW, len: GD, pitch: PITCH, y0: GH + 1, matFront: roofG1, matBack: roofG2, capMat: M.fascia, fasciaMat: M.fascia });
gRoofGrp.add(gRoof.g);
gRoofGrp.rotation.y = Math.PI / 2;
gRoofGrp.position.set(gx, 0, gz);
g.add(gRoofGrp);
const pedF = pediment(M, M.sidingWhite, GW, grise, 0, GH + 1, false);
pedF.position.set(gx, 0, MD / 2 - 0.01);
g.add(pedF);
put(g, doorUnit(M, M.garage, 16, 7.6), gx, 4.9, MD / 2 + 0.1);
put(g, chimney(M, WH + rise + 5, -MW / 2 - 1.7, -4));
/* porch over the front door */
const px = -8;
g.add(box(9.5, 0.9, 5.5, M.concrete, px, 0.45, MD / 2 + 2.6));
[[-4, 2.2], [4, 2.2], [-4, 4.9], [4, 4.9]].forEach((pp) =>
g.add(box(0.55, 8.6, 0.55, M.trim, px + pp[0], 5.2, MD / 2 + pp[1])));
const pr = box(11, 0.4, 7, M.porchRoof, px, 10.2, MD / 2 + 2.8);
pr.rotation.x = 0.12;
g.add(pr);
put(g, doorUnit(M, M.doorRed), px, 4.45, MD / 2 + 0.12);
/* windows */
const F = MD / 2 + 0.1;
[-16, -8, 0, 8, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, F));
[-16, 0, 8, 16].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
[-14, -4, 6, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, -F));
[-14, 4, 14].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, -F));
[-6, 6].forEach((z) => put(g, windowUnit(M), -MW / 2 - 0.1, 15.6, z, -Math.PI / 2));
put(g, windowUnit(M, 2.6, 3.4), gx + GW / 2 + 0.1, 6, gz, Math.PI / 2);
/* grounds */
g.add(box(17, 0.14, 30, M.asphalt, gx, 0.08, MD / 2 + 15));
g.add(box(3.4, 0.12, 26, M.concrete, px, 0.07, MD / 2 + 13));
put(g, mailbox(M), px + 5, 0, 41);
put(g, tree(M, 1.15), -33, 0, -15);
put(g, tree(M, 0.9), 29, 0, -24);
put(g, tree(M, 0.75), -31, 0, 14);
[-18, -13, -3, 3, 13, 18].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.18), x, 0, MD / 2 + 2.2));
const area = Math.round(roof.area + gRoof.area);
return {
group: g,
measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + gRoof.capLf), facets: 5 },
anchors: {
spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
},
hl: {
ridge: hlFromSegs(M, [roof.ridgeSeg, [v(gx, gRoof.ridgeY + 0.5, gz - GD / 2 - 1.2), v(gx, gRoof.ridgeY + 0.5, gz + GD / 2 + 1.2)]]),
eave: hlFromSegs(M, roof.eaveSegs),
roof: roofWash,
roofMats: [roofF, roofB, roofG1, roofG2],
damage: null,
},
};
}
export function buildHip(M) {
const g = new THREE.Group();
const RW = 46, RD = 26, WH = 10.5, PITCH = 6;
const roofM = M.shingleWarm.clone(), roofG = M.shingleWarm.clone(), roofS = M.shingleWarm.clone();
g.add(box(RW + 1, 1.2, RD + 1, M.concrete, 0, 0.6, 0));
g.add(box(RW, WH, RD, M.sidingBeige, 0, WH / 2 + 1, 0));
const roof = hipRoof({ span: RD, len: RW, pitch: PITCH, y0: WH + 1, mat: roofM, capMat: M.fascia, fasciaMat: M.fascia });
g.add(roof.g);
const roofWash = hlRoofWash(M, roof, true);
g.add(roofWash);
/* attached hip garage, protruding toward the street */
const GW = 20, GH = 9;
const gx = 14, gz = RD / 2 + GW / 2 - 1;
g.add(box(GW + 0.8, 1.1, GW + 0.8, M.concrete, gx, 0.55, gz));
g.add(box(GW, GH, GW, M.sidingBeige, gx, GH / 2 + 1, gz));
const gRoof = hipRoof({ span: GW, len: GW, pitch: PITCH, y0: GH + 1, mat: roofG, capMat: M.fascia, fasciaMat: M.fascia });
gRoof.g.position.set(gx, 0, gz);
g.add(gRoof.g);
put(g, doorUnit(M, M.garage, 15, 7.2), gx, 4.7, gz + GW / 2 + 0.1);
/* windows + door on the clear side of the facade */
const F = RD / 2 + 0.1;
put(g, doorUnit(M, M.doorBlue), -2, 4.45, F);
[-14.5, -11.2, -7.9].forEach((x) => put(g, windowUnit(M, 2.9, 4.2), x, 6, F));
put(g, windowUnit(M, 3.2, 4.2), -19.5, 6, F);
[-12, 0, 12].forEach((x) => put(g, windowUnit(M, 3.2, 4.2), x, 6, -F));
[-4, 5].forEach((z) => put(g, windowUnit(M, 3, 4), -RW / 2 - 0.1, 6, z, -Math.PI / 2));
/* backyard shed */
const shed = new THREE.Group();
shed.add(box(10, 6.5, 8, M.sidingGray, 0, 3.25, 0));
const sr = gableRoof({ span: 8, len: 10, pitch: 6, oE: 0.8, oG: 0.7, y0: 6.5, matFront: roofS, matBack: roofS, capMat: M.fascia, fasciaMat: M.fascia });
shed.add(sr.g);
shed.add(pediment(M, M.sidingGray, 8, 2, 5, 6.5, true));
shed.add(pediment(M, M.sidingGray, 8, 2, -5, 6.5, true));
put(shed, doorUnit(M, M.doorBlue, 2.6, 5.4), 0, 3.7, 4.1);
shed.position.set(-26, 0, -19);
shed.rotation.y = 0.35;
g.add(shed);
/* rear terrace with pergola */
g.add(box(18, 0.3, 12, M.concrete, 4, 0.15, -RD / 2 - 6));
[[-8, -1.5], [8, -1.5], [-8, -10.5], [8, -10.5]].forEach((pp) =>
g.add(box(0.6, 8.4, 0.6, M.trim, 4 + pp[0], 4.2, -RD / 2 + pp[1])));
for (let i = 0; i < 5; i++) g.add(box(18.5, 0.22, 0.9, M.trim, 4, 8.5, -RD / 2 - 2 - i * 2.1));
/* grounds */
g.add(box(17, 0.14, 13, M.asphalt, gx, 0.08, gz + GW / 2 + 6.5));
g.add(box(3, 0.12, 29, M.concrete, -2, 0.07, F + 14.5));
put(g, mailbox(M), 3, 0, 42);
put(g, tree(M, 1.3), -31, 0, 8);
put(g, tree(M, 1), 30, 0, -14);
[-16, -6, 2].forEach((x, i) => put(g, shrub(M, 1 + i * 0.15), x, 0, F + 1.8));
return {
group: g,
measure: { roofSqFt: Math.round(roof.area + gRoof.area + sr.area), ridgeLf: Math.round(roof.capLf + gRoof.capLf + sr.capLf), facets: 10 },
anchors: {
spanA: v(-RW / 2, 0.4, RD / 2), spanB: v(RW / 2, 0.4, RD / 2),
peakTop: v(-(RW - RD) / 2, roof.ridgeY + 0.8, 0), peakBase: v(-(RW - RD) / 2, 0, 0),
pitchAt: v(-RW / 2 - 1.5, roof.eaveY, RD / 2 + 1.5),
},
hl: {
ridge: hlFromSegs(M, [roof.ridgeSeg].concat(roof.hipSegs)),
eave: hlFromSegs(M, roof.eaveSegs),
roof: roofWash,
roofMats: [roofM, roofG, roofS],
damage: null,
},
};
}
export function buildStorm(M) {
const g = new THREE.Group();
const FW = 58, FD = 28, WH = 18, PITCH = 5;
const rise = (FD / 2) * (PITCH / 12);
const roofF = M.shingleStorm.clone(), roofB = M.shingle.clone(), roofW = M.shingle.clone();
g.add(box(FW + 1, 1.4, FD + 1, M.concrete, 0, 0.7, 0));
g.add(box(FW, WH, FD, M.sidingGray, 0, WH / 2 + 1, 0));
g.add(pediment(M, M.sidingGray, FD, rise, FW / 2, WH + 1, true));
g.add(pediment(M, M.sidingGray, FD, rise, -FW / 2, WH + 1, true));
const roof = gableRoof({ span: FD, len: FW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
g.add(roof.g);
const roofWash = hlRoofWash(M, roof, false);
g.add(roofWash);
put(g, chimney(M, WH + rise + 6, 7, 0));
/* rear single-storey wing (the L that makes this a 30-square claim) */
const WGW = 24, WGL = 20, WGH = 10;
const wx = -8, wz = -FD / 2 - WGL / 2 + 0.5;
g.add(box(WGW + 0.8, 1.2, WGL + 0.8, M.concrete, wx, 0.6, wz));
g.add(box(WGW, WGH, WGL, M.sidingGray, wx, WGH / 2 + 1, wz));
const wrise = (WGW / 2) * (PITCH / 12);
const wRoofGrp = new THREE.Group();
const wRoof = gableRoof({ span: WGW, len: WGL, pitch: PITCH, y0: WGH + 1, matFront: roofW, matBack: roofW, capMat: M.fascia, fasciaMat: M.fascia });
wRoofGrp.add(wRoof.g);
wRoofGrp.rotation.y = Math.PI / 2;
wRoofGrp.position.set(wx, 0, wz);
g.add(wRoofGrp);
const wPed = pediment(M, M.sidingGray, WGW, wrise, 0, WGH + 1, false);
wPed.position.set(wx, 0, wz - WGL / 2 - 0.01);
wPed.rotation.y = Math.PI;
g.add(wPed);
[-6, 6].forEach((dx) => put(g, windowUnit(M, 3, 4), wx + dx, 6, wz - WGL / 2 - 0.1, Math.PI));
/* full-width front porch */
const F = FD / 2;
g.add(box(FW - 2, 0.9, 7, M.concrete, 0, 0.45, F + 3.4));
[-26, -13, 0, 13, 26].forEach((x) => g.add(box(0.6, 9.3, 0.6, M.trim, x, 5.55, F + 6.2)));
const pr = box(FW, 0.4, 8.6, M.porchRoof, 0, 11, F + 3.6);
pr.rotation.x = 0.1;
g.add(pr);
put(g, doorUnit(M, M.doorBlue, 3.4, 7), 0, 4.5, F + 0.12);
[-23, -11.5, 11.5, 23].forEach((x) => put(g, windowUnit(M, 3.2, 4.6), x, 6.4, F + 0.1));
[-23, -11.5, 0, 11.5, 23].forEach((x) => put(g, windowUnit(M), x, 15.4, F + 0.1));
[-25, 9, 17, 25].forEach((x) => put(g, windowUnit(M), x, 15.4, -F - 0.1));
[10, 18, 26].forEach((x) => put(g, windowUnit(M, 3.2, 4.6), x, 6.4, -F - 0.1));
[-5, 5].forEach((z) => put(g, windowUnit(M, 3, 4.2), -FW / 2 - 0.1, 14, z, -Math.PI / 2));
/* tarped section on the damaged plane */
const p = PITCH / 12;
const ry = WH + 1 + rise;
const tarp = mk(quadGeo(
v(9, ry + 0.55, 0.4), v(21, ry + 0.55, 0.4),
v(21.6, ry - 8.2 * p + 0.55, 8.6), v(8.4, ry - 8.2 * p + 0.55, 8.6)
), M.tarp);
g.add(tarp);
/* 41 hail impacts clustered on the front plane (facet F2) */
const impactMat = M.impact.clone();
const imp = new THREE.InstancedMesh(new THREE.SphereGeometry(0.36, 8, 6), impactMat, 41);
const dm = new THREE.Matrix4();
let sd = 4021;
const rr = () => ((sd = (sd * 16807) % 2147483647) / 2147483647);
for (let i = 0; i < 41; i++) {
const cl = rr() < 0.62;
const x = cl ? -18 + rr() * 20 : -28 + rr() * 56;
const t = 0.12 + rr() * 0.8;
const z = t * (FD / 2 + 1.2);
const y = ry - z * p + 0.45;
dm.makeScale(1, 0.4, 1);
dm.setPosition(x, y, z);
imp.setMatrixAt(i, dm);
}
imp.castShadow = false;
g.add(imp);
/* storm-yard: debris, fallen limb */
let s2 = 99;
const r2 = () => ((s2 = (s2 * 48271) % 2147483647) / 2147483647);
for (let i = 0; i < 6; i++) {
const d = box(1.6 + r2() * 2, 0.18, 1 + r2(), M.porchRoof, -20 + r2() * 44, 0.1, F + 8 + r2() * 14);
d.rotation.y = r2() * 3;
g.add(d);
}
const limb = mk(new THREE.CylinderGeometry(0.35, 0.5, 9, 6), M.trunk);
limb.rotation.z = Math.PI / 2 - 0.06;
limb.rotation.y = 0.5;
limb.position.set(27, 0.5, 16);
g.add(limb);
g.add(box(13, 0.14, 30, M.asphalt, 30, 0.08, F + 16));
g.add(box(3.2, 0.12, 22, M.concrete, 0, 0.07, F + 18));
put(g, mailbox(M), -5, 0, 42);
put(g, tree(M, 1.2), -34, 0, -10);
put(g, tree(M, 1.05), 34, 0, -18);
return {
group: g,
measure: { roofSqFt: Math.round(roof.area + wRoof.area), ridgeLf: Math.round(roof.capLf + wRoof.capLf), facets: 4 },
anchors: {
spanA: v(-FW / 2, 0.4, FD / 2), spanB: v(FW / 2, 0.4, FD / 2),
peakTop: v(-FW / 2, ry + 0.7, 0), peakBase: v(-FW / 2, 0, 0),
pitchAt: v(-FW / 2 - 1.5, roof.eaveY, FD / 2 + 1.5),
damageAt: v(-8, ry - 4 * p, 4.5),
},
hl: {
ridge: hlFromSegs(M, [roof.ridgeSeg]),
eave: hlFromSegs(M, roof.eaveSegs),
roof: roofWash,
roofMats: [roofF, roofB, roofW],
damage: { mesh: imp, mat: impactMat, base: 0.9 },
},
};
}
/* multi-wing estate: two perpendicular gable wings plus a rear nub, wrapping a
courtyard pool — modelled after a client-supplied aerial survey of a complex
multi-facet roof (5705 Bent Oak Place, Dallas TX). */
export function buildEstate(M) {
const g = new THREE.Group();
const MW = 42, MD = 28, WH = 19, PITCH = 6;
const rise = (MD / 2) * (PITCH / 12);
const roofF = M.shingle.clone(), roofB = M.shingle.clone();
const roofW1 = M.shingle.clone(), roofW2 = M.shingle.clone();
const roofN1 = M.shingle.clone(), roofN2 = M.shingle.clone();
g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0)); /* foundation plinth */
g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
g.add(roof.g);
const roofWash = hlRoofWash(M, roof, false);
g.add(roofWash);
put(g, chimney(M, WH + rise + 5, -MW / 2 - 1.7, -4));
/* wing A: perpendicular guest wing, front-flush, closes one side of the courtyard */
const GW = 20, GD = 22, GH = 10;
const gx = MW / 2 + GW / 2 - 0.2, gz = MD / 2 - GD / 2;
g.add(box(GW + 0.8, 1.2, GD + 0.8, M.concrete, gx, 0.6, gz));
g.add(box(GW, GH, GD, M.sidingWhite, gx, GH / 2 + 1, gz));
const grise = (GW / 2) * (PITCH / 12);
const gRoofGrp = new THREE.Group();
const gRoof = gableRoof({ span: GW, len: GD, pitch: PITCH, y0: GH + 1, matFront: roofW1, matBack: roofW2, capMat: M.fascia, fasciaMat: M.fascia });
gRoofGrp.add(gRoof.g);
gRoofGrp.rotation.y = Math.PI / 2;
gRoofGrp.position.set(gx, 0, gz);
g.add(gRoofGrp);
const pedF = pediment(M, M.sidingWhite, GW, grise, 0, GH + 1, false);
pedF.position.set(gx, 0, MD / 2 - 0.01);
g.add(pedF);
put(g, doorUnit(M, M.doorBlue, 3, 6.6), gx, 4.4, MD / 2 + 0.1);
[-6, 6].forEach((z) => put(g, windowUnit(M, 2.8, 4), gx + GW / 2 + 0.1, 6, gz + z, Math.PI / 2));
/* wing B: small rear nub off the main ridge, the extra facet visible in the survey */
const NW = 14, ND = 12, NH = 8;
const nx = -MW / 2 + NW / 2 + 3, nz = -MD / 2 - ND / 2 + 1;
g.add(box(NW + 0.6, 1, ND + 0.6, M.concrete, nx, 0.5, nz));
g.add(box(NW, NH, ND, M.sidingWhite, nx, NH / 2 + 1, nz));
const nRoofGrp = new THREE.Group();
const nRoof = gableRoof({ span: ND, len: NW, pitch: PITCH, oE: 1, oG: 1, y0: NH + 1, matFront: roofN1, matBack: roofN2, capMat: M.fascia, fasciaMat: M.fascia });
nRoofGrp.add(nRoof.g);
nRoofGrp.position.set(nx, 0, nz);
g.add(nRoofGrp);
put(g, windowUnit(M, 2.4, 3.4), nx, 5.6, nz - ND / 2 - 0.1, Math.PI);
/* pool, nestled in the courtyard the wing creates */
g.add(box(14, 0.3, 7, M.concrete, gx - 2, -0.05, -10));
/* porch over the front door */
const px = -8;
g.add(box(9.5, 0.9, 5.5, M.concrete, px, 0.45, MD / 2 + 2.6));
[[-4, 2.2], [4, 2.2], [-4, 4.9], [4, 4.9]].forEach((pp) =>
g.add(box(0.55, 8.6, 0.55, M.trim, px + pp[0], 5.2, MD / 2 + pp[1])));
const pr = box(11, 0.4, 7, M.porchRoof, px, 10.2, MD / 2 + 2.8);
pr.rotation.x = 0.12;
g.add(pr);
put(g, doorUnit(M, M.doorRed), px, 4.45, MD / 2 + 0.12);
/* windows */
const F = MD / 2 + 0.1;
[-16, -8, 0, 8, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, F));
[-16, 0, 8, 16].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
[-14, -4, 6, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, -F));
[-14, 4, 14].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, -F));
[-6, 6].forEach((z) => put(g, windowUnit(M), -MW / 2 - 0.1, 15.6, z, -Math.PI / 2));
/* grounds */
g.add(box(17, 0.14, 30, M.asphalt, gx, 0.08, MD / 2 + 15));
g.add(box(3.4, 0.12, 26, M.concrete, px, 0.07, MD / 2 + 13));
put(g, mailbox(M), px + 5, 0, 41);
put(g, tree(M, 1.15), -33, 0, -15);
put(g, tree(M, 0.9), 29, 0, -24);
put(g, tree(M, 0.75), -31, 0, 14);
[-18, -13, -3, 3, 13, 18].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.18), x, 0, MD / 2 + 2.2));
const area = Math.round(roof.area + gRoof.area + nRoof.area);
return {
group: g,
measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + gRoof.capLf + nRoof.capLf), facets: 6 },
anchors: {
spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
},
hl: {
ridge: hlFromSegs(M, [roof.ridgeSeg, [v(gx, gRoof.ridgeY + 0.5, gz - GD / 2 - 1.2), v(gx, gRoof.ridgeY + 0.5, gz + GD / 2 + 1.2)]]),
eave: hlFromSegs(M, roof.eaveSegs),
roof: roofWash,
roofMats: [roofF, roofB, roofW1, roofW2, roofN1, roofN2],
damage: null,
},
};
}
/* steep 12/12 multi-wing estate with a detached outbuilding, modelled after a client-supplied
EagleView survey (12552 Mustang Circle, Forney TX): 6,490 sq ft across 37 facets on the real
roof, simplified here to a cross-wing main house plus a separate garage while keeping the
signature 12/12 pitch and split-structure footprint. */
export function buildMustang(M) {
const g = new THREE.Group();
const MW = 32, MD = 24, WH = 13, PITCH = 12;
const rise = (MD / 2) * (PITCH / 12);
const roofF = M.shingle.clone(), roofB = M.shingle.clone();
const roofW1 = M.shingle.clone(), roofW2 = M.shingle.clone();
const roofX1 = M.shingle.clone(), roofX2 = M.shingle.clone();
const roofN1 = M.shingle.clone(), roofN2 = M.shingle.clone();
const roofG1 = M.shingle.clone(), roofG2 = M.shingle.clone();
g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0));
g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
g.add(roof.g);
const roofWash = hlRoofWash(M, roof, false);
g.add(roofWash);
put(g, chimney(M, WH + rise + 6, 10, -MD / 2 - 2));
/* wing A: front-right, its own steep gable, offset like the real survey's radiating wings */
const AW = 18, AD = 20, AH = 11;
const ax = MW / 2 + AW / 2 - 0.2, az = MD / 2 - AD / 2 - 2;
g.add(box(AW + 0.8, 1.2, AD + 0.8, M.concrete, ax, 0.6, az));
g.add(box(AW, AH, AD, M.sidingWhite, ax, AH / 2 + 1, az));
const arise = (AW / 2) * (PITCH / 12);
const aRoofGrp = new THREE.Group();
const aRoof = gableRoof({ span: AW, len: AD, pitch: PITCH, y0: AH + 1, matFront: roofW1, matBack: roofW2, capMat: M.fascia, fasciaMat: M.fascia });
aRoofGrp.add(aRoof.g);
aRoofGrp.rotation.y = Math.PI / 2;
aRoofGrp.position.set(ax, 0, az);
g.add(aRoofGrp);
const pedA = pediment(M, M.sidingWhite, AW, arise, 0, AH + 1, false);
pedA.position.set(ax, 0, az + AD / 2 - 0.01);
g.add(pedA);
[-4, 4].forEach((z) => put(g, windowUnit(M, 2.8, 4), ax + AW / 2 + 0.1, 6, az + z, Math.PI / 2));
/* wing B: rear-left, mirrored offset, so the roofline radiates like the real survey's cross plan */
const BW = 16, BD = 18, BH = 10;
const bx = -MW / 2 - BW / 2 + 0.2, bz = -MD / 2 + BD / 2 + 2;
g.add(box(BW + 0.8, 1.2, BD + 0.8, M.concrete, bx, 0.6, bz));
g.add(box(BW, BH, BD, M.sidingWhite, bx, BH / 2 + 1, bz));
const brise = (BW / 2) * (PITCH / 12);
const bRoofGrp = new THREE.Group();
const bRoof = gableRoof({ span: BW, len: BD, pitch: PITCH, y0: BH + 1, matFront: roofX1, matBack: roofX2, capMat: M.fascia, fasciaMat: M.fascia });
bRoofGrp.add(bRoof.g);
bRoofGrp.rotation.y = Math.PI / 2;
bRoofGrp.position.set(bx, 0, bz);
g.add(bRoofGrp);
const pedB = pediment(M, M.sidingWhite, BW, brise, 0, BH + 1, false);
pedB.position.set(bx, 0, bz - BD / 2 + 0.01);
pedB.rotation.y = Math.PI;
g.add(pedB);
[-4, 4].forEach((z) => put(g, windowUnit(M, 2.6, 3.6), bx - BW / 2 - 0.1, 5.8, bz + z, -Math.PI / 2));
/* rear nub: the extra facet visible in the survey's back roofline */
const NW = 12, ND = 11, NH = 8;
const nx = 2, nz = -MD / 2 - ND / 2 + 1;
g.add(box(NW + 0.6, 1, ND + 0.6, M.concrete, nx, 0.5, nz));
g.add(box(NW, NH, ND, M.sidingWhite, nx, NH / 2 + 1, nz));
const nRoofGrp = new THREE.Group();
const nRoof = gableRoof({ span: ND, len: NW, pitch: PITCH, oE: 1, oG: 1, y0: NH + 1, matFront: roofN1, matBack: roofN2, capMat: M.fascia, fasciaMat: M.fascia });
nRoofGrp.add(nRoof.g);
nRoofGrp.position.set(nx, 0, nz);
g.add(nRoofGrp);
put(g, windowUnit(M, 2.4, 3.2), nx, 5.4, nz - ND / 2 - 0.1, Math.PI);
/* front door + small stoop; a full porch roof would clip a pitch this steep */
g.add(box(6, 0.7, 4, M.concrete, -6, 0.35, MD / 2 + 2));
put(g, doorUnit(M, M.doorRed), -6, 4.45, MD / 2 + 0.12);
/* windows */
const F = MD / 2 + 0.1;
[-12, -4, 4, 12].forEach((x) => put(g, windowUnit(M), x, 15.2, F));
[-12, 12].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
[-10, 0, 10].forEach((x) => put(g, windowUnit(M), x, 15.2, -F));
/* detached garage: a separate outbuilding on a shallow 4/12 pitch, matching the survey's Structure #2 */
const GW = 20, GD = 18, GH = 8, GP = 4;
const gx = -MW / 2 - 26, gz = 6;
const garage = new THREE.Group();
garage.add(box(GW + 0.8, 1, GD + 0.8, M.concrete, 0, 0.5, 0));
garage.add(box(GW, GH, GD, M.sidingGray, 0, GH / 2 + 0.5, 0));
const gRoof = gableRoof({ span: GD, len: GW, pitch: GP, oE: 1.3, oG: 1, y0: GH + 0.5, matFront: roofG1, matBack: roofG2, capMat: M.fascia, fasciaMat: M.fascia });
garage.add(gRoof.g);
put(garage, doorUnit(M, M.garage, 14, 7), 0, 4.4, GD / 2 + 0.1);
garage.position.set(gx, 0, gz);
g.add(garage);
/* grounds */
g.add(box(16, 0.14, 34, M.asphalt, gx + 4, 0.08, gz + 20));
g.add(box(3, 0.12, 20, M.concrete, -6, 0.07, MD / 2 + 12));
put(g, mailbox(M), -3, 0, 38);
put(g, tree(M, 1.3), -40, 0, -18);
put(g, tree(M, 1), 26, 0, -22);
put(g, tree(M, 0.85), -18, 0, 30);
[-14, -6, 6, 14].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.15), x, 0, MD / 2 + 2));
const area = Math.round(roof.area + aRoof.area + bRoof.area + nRoof.area + gRoof.area);
return {
group: g,
measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + aRoof.capLf + bRoof.capLf + nRoof.capLf + gRoof.capLf), facets: 12 },
anchors: {
spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
},
hl: {
ridge: hlFromSegs(M, [
roof.ridgeSeg,
[v(ax, aRoof.ridgeY + 0.5, az - AD / 2 - 1.2), v(ax, aRoof.ridgeY + 0.5, az + AD / 2 + 1.2)],
[v(bx, bRoof.ridgeY + 0.5, bz - BD / 2 - 1.2), v(bx, bRoof.ridgeY + 0.5, bz + BD / 2 + 1.2)],
]),
eave: hlFromSegs(M, roof.eaveSegs),
roof: roofWash,
roofMats: [roofF, roofB, roofW1, roofW2, roofX1, roofX2, roofN1, roofN2, roofG1, roofG2],
damage: null,
},
};
}
+349
View File
@@ -0,0 +1,349 @@
import * as THREE from "three";
import gsap from "gsap";
import { makeMaterials } from "./textures.js";
import { buildGable, buildHip, buildStorm, buildEstate, buildMustang } from "./houses.js";
/*
* Hidden-line blueprint renderer.
* Faces render as opaque "paper" (near the page canvas colour) so nearer
* geometry occludes the linework behind it, exactly like a technical drawing.
* Edges are extracted per mesh and drawn in bone ink at three weights.
* A pair of opposing clipping planes splits every ink line into "inked"
* (behind the scan) and "pencil ghost" (ahead of it), so the LiDAR pass
* literally draws the house, and chip swaps replay the same plotter wipe.
*/
const PAPER = 0x11141a;
const INK = 0xdfe4ea;
const AMBER = 0xf2a83b;
export function createScene(mount, { reduced = false } = {}) {
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
renderer.localClippingEnabled = true;
renderer.domElement.style.cssText = "width:100%;height:100%;display:block";
mount.prepend(renderer.domElement);
const scene = new THREE.Scene();
let W = 880, H = 344;
const camera = new THREE.PerspectiveCamera(33, W / H, 1, 500);
const M = makeMaterials();
/* ---- ink pipeline ------------------------------------------------------ */
const clipInk = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 1e6); /* keeps x < c */
const clipGhost = new THREE.Plane(new THREE.Vector3(1, 0, 0), -1e6); /* keeps x > c */
const lineMat = (opacity, color = INK, clipped = true) =>
new THREE.LineBasicMaterial({
color, transparent: true, opacity,
clippingPlanes: clipped ? [clipInk] : null,
});
const inks = { primary: lineMat(0.95), roofBase: null, detail: lineMat(0.52), site: lineMat(0.26) };
const ghost = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.16, clippingPlanes: [clipGhost] });
const inkRed = new THREE.LineBasicMaterial({ color: 0xff6644, transparent: true, opacity: 0.9, clippingPlanes: [clipInk] });
const paper = new THREE.MeshBasicMaterial({ color: PAPER, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
/* dusk: window glass carries a faint interior warmth without leaving the ink language */
const glassGlow = new THREE.MeshBasicMaterial({ color: 0x30260e, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
function setScanX(x) { clipInk.constant = x; clipGhost.constant = -x; }
setScanX(reduced ? 1e6 : -78);
/* ---- houses, converted to hidden-line ---------------------------------- */
const houses = { gable: buildGable(M), hip: buildHip(M), storm: buildStorm(M), estate: buildEstate(M), mustang: buildMustang(M) };
let styleKey = "gable";
function classify(m, house, inherited) {
if (inherited) return inherited;
const mat = m.material;
if (house.hl.roofMats.includes(mat)) return "roof";
if (mat === M.tarp) return "tarp";
if (mat === M.trunk || mat === M.foliage || mat === M.foliage2) return "sitewire";
if (mat === M.asphalt) return "siteflat";
if (mat === M.glass || mat === M.trim || mat === M.doorRed || mat === M.doorBlue || mat === M.garage) return "detail";
const h = m.geometry && m.geometry.parameters ? m.geometry.parameters.height : undefined;
if (mat === M.concrete) return h !== undefined && h <= 0.35 ? "siteflat" : "primary";
if (mat === M.porchRoof) return h !== undefined && h <= 0.25 ? "site" : "primary";
return "primary";
}
function convert(house) {
house.roofLineMats = [];
const jobs = [];
(function walk(o, inherited) {
const tag = o.userData && o.userData.ink ? o.userData.ink : inherited;
if (tag === "skip") return;
if (o.isMesh) jobs.push([o, tag]);
const kids = o.children.slice();
for (const c of kids) walk(c, tag);
})(house.group, null);
for (const [m, tag] of jobs) {
if (m.isInstancedMesh) {
const im = new THREE.MeshBasicMaterial({ color: 0xff5030 });
m.material = im;
if (house.hl.damage) { house.hl.damage.mat = im; }
continue;
}
const level = classify(m, house, tag);
const eg = new THREE.EdgesGeometry(m.geometry, 30);
let mat;
if (level === "roof") {
mat = lineMat(0.95);
house.roofLineMats.push(mat);
} else if (level === "tarp") {
mat = inkRed;
} else {
mat = inks[level === "siteflat" || level === "sitewire" ? "site" : level] || inks.primary;
}
const ln = new THREE.LineSegments(eg, mat);
ln.position.copy(m.position);
ln.rotation.copy(m.rotation);
ln.scale.copy(m.scale);
m.parent.add(ln);
if (!reduced) {
const gl = new THREE.LineSegments(eg, ghost);
gl.position.copy(m.position);
gl.rotation.copy(m.rotation);
gl.scale.copy(m.scale);
m.parent.add(gl);
}
if (level === "siteflat" || level === "sitewire" || level === "tarp") m.visible = false;
else if (m.material === M.glass) m.material = glassGlow;
else m.material = paper;
}
}
for (const k of Object.keys(houses)) {
const h = houses[k];
h.hl.ridge.userData.ink = "skip";
h.hl.eave.userData.ink = "skip";
h.group.add(h.hl.ridge, h.hl.eave);
convert(h);
h.group.visible = k === styleKey;
scene.add(h.group);
}
/* ---- site plan linework (scene level, unclipped) ------------------------ */
const sitePlan = new THREE.Group();
{
const gridMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.06 });
const pts = [];
const R = 50;
for (let g = -R; g <= R; g += 10) {
const m = Math.sqrt(Math.max(0, R * R - g * g));
pts.push(g, 0, -m, g, 0, m, -m, 0, g, m, 0, g);
}
const gg = new THREE.BufferGeometry();
gg.setAttribute("position", new THREE.BufferAttribute(new Float32Array(pts), 3));
sitePlan.add(new THREE.LineSegments(gg, gridMat));
/* dashed property boundary */
const bMat = new THREE.LineDashedMaterial({ color: INK, transparent: true, opacity: 0.3, dashSize: 2.2, gapSize: 2 });
const bx = 52, bz = 41;
const bGeo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-bx, 0, -bz), new THREE.Vector3(bx, 0, -bz),
new THREE.Vector3(bx, 0, bz), new THREE.Vector3(-bx, 0, bz),
]);
const bound = new THREE.LineLoop(bGeo, bMat);
bound.computeLineDistances();
sitePlan.add(bound);
/* survey crosses at the corners */
const cMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.4 });
const cp = [];
[[-bx, -bz], [bx, -bz], [bx, bz], [-bx, bz]].forEach(([x, z]) => {
cp.push(x - 1.6, 0, z, x + 1.6, 0, z, x, 0, z - 1.6, x, 0, z + 1.6);
});
const cGeo = new THREE.BufferGeometry();
cGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(cp), 3));
sitePlan.add(new THREE.LineSegments(cGeo, cMat));
/* street edges + centre dashes */
const sMat = new THREE.LineBasicMaterial({ color: INK, transparent: true, opacity: 0.2 });
const sGeo = new THREE.BufferGeometry();
sGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array([
-62, 0, 44, 62, 0, 44, -62, 0, 57, 62, 0, 57,
]), 3));
sitePlan.add(new THREE.LineSegments(sGeo, sMat));
const dashes = [];
for (let x = -58; x < 58; x += 9) dashes.push(x, 0, 50.5, x + 4, 0, 50.5);
const dGeo = new THREE.BufferGeometry();
dGeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(dashes), 3));
sitePlan.add(new THREE.LineSegments(dGeo, sMat));
sitePlan.position.y = 0.01;
}
scene.add(sitePlan);
/* soft contact blob keeps the mass grounded */
const contact = new THREE.Mesh(new THREE.CircleGeometry(40, 48), M.contact);
contact.rotation.x = -Math.PI / 2;
contact.position.y = 0.005;
contact.material.opacity = 0.32;
contact.scale.set(1.25, 0.85, 1);
scene.add(contact);
/* ---- scan sheet visuals -------------------------------------------------- */
const sheet = new THREE.Mesh(new THREE.PlaneGeometry(140, 44), M.scan);
sheet.rotation.y = Math.PI / 2;
sheet.position.y = 18;
sheet.visible = false;
sheet.renderOrder = 20;
scene.add(sheet);
const scanEdge = new THREE.Mesh(
new THREE.BoxGeometry(0.22, 50, 128),
new THREE.MeshBasicMaterial({ color: 0xffcf55, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false })
);
scanEdge.position.y = 22;
scanEdge.visible = false;
scanEdge.renderOrder = 21;
scene.add(scanEdge);
/* ---- camera orbit -------------------------------------------------------- */
const target = new THREE.Vector3(3, 9.5, 0);
let az = -0.62, el = 0.27, radius = 96;
let vel = 0.00115, dragVel = 0, dragging = false;
function placeCamera() {
camera.position.set(
target.x + radius * Math.cos(el) * Math.sin(az),
target.y + radius * Math.sin(el),
target.z + radius * Math.cos(el) * Math.cos(az)
);
camera.lookAt(target);
}
placeCamera();
const cvs = renderer.domElement;
cvs.style.cursor = "grab";
let lastX = 0, lastT = 0;
cvs.addEventListener("pointerdown", (e) => {
dragging = true;
lastX = e.clientX;
lastT = performance.now();
cvs.setPointerCapture(e.pointerId);
cvs.style.cursor = "grabbing";
onDrag();
});
cvs.addEventListener("pointermove", (e) => {
if (!dragging) return;
const now = performance.now();
const dx = e.clientX - lastX;
az -= dx * 0.008;
dragVel = (dx / Math.max(now - lastT, 1)) * 0.11;
lastX = e.clientX;
lastT = now;
});
const up = () => {
if (!dragging) return;
dragging = false;
cvs.style.cursor = "grab";
vel = Math.max(-0.2, Math.min(0.2, -dragVel));
dragVel = 0;
};
cvs.addEventListener("pointerup", up);
cvs.addEventListener("pointercancel", up);
let onDrag = () => {};
/* ---- scan scrub + plotter wipe ------------------------------------------- */
let stageT = 0;
const swapWipe = { active: false, x: -80 };
function scanUpdate() {
const t = stageT;
const sa = Math.min(1, Math.max(0, (t - 0.03) / 0.05)) * (1 - Math.min(1, Math.max(0, (t - 0.76) / 0.15)));
const sp = Math.min(1, Math.max(0, (t - 0.05) / 0.28));
const on = sa > 0.01 && sp > 0 && sp < 1;
sheet.visible = on;
scanEdge.visible = on;
if (on) {
const x = -78 + 156 * sp;
sheet.position.x = x;
scanEdge.position.x = x;
sheet.material.opacity = 0.16 * sa;
scanEdge.material.opacity = 0.5 * sa;
}
if (reduced) { setScanX(1e6); return; }
if (swapWipe.active) setScanX(swapWipe.x);
else setScanX(-78 + 156 * sp);
}
/* ---- highlights ----------------------------------------------------------- */
function applyHighlight(k) {
for (const key of Object.keys(houses)) {
const h = houses[key];
const on = key === styleKey;
h.hl.ridge.visible = on && k === "ridge";
h.hl.eave.visible = on && k === "eave";
if (h.hl.roof) h.hl.roof.visible = on && k === "roof";
(h.roofLineMats || []).forEach((m) => m.color.setHex(on && k === "roof" ? AMBER : INK));
if (h.hl.damage) h.hl.damage.mat.color.setHex(on && k === "damage" ? 0xff2612 : 0xff5030);
}
}
/* ---- loop ------------------------------------------------------------------ */
let visible = true;
if ("IntersectionObserver" in window) new IntersectionObserver((en) => { visible = en[0].isIntersecting; }).observe(cvs);
let frameCb = null;
let prev = performance.now();
renderer.setAnimationLoop((now) => {
const dt = Math.min(now - prev, 50);
prev = now;
if (!visible) return;
if (!reduced && !dragging) {
vel += (0.00115 - vel) * 0.02;
az += (vel * dt) / 16.7;
}
placeCamera();
scanUpdate();
if (frameCb) frameCb();
renderer.render(scene, camera);
});
return {
el: cvs,
onFrame(cb) { frameCb = cb; },
onDragStart(cb) { onDrag = cb; },
resize(w, h) {
W = Math.max(2, Math.round(w));
H = Math.max(2, Math.round(h));
renderer.setSize(W, H, false);
camera.aspect = W / H;
camera.updateProjectionMatrix();
},
measure({ style }) {
return houses[style]?.measure || houses.gable.measure;
},
anchors() { return houses[styleKey].anchors; },
project(v3) {
const p = v3.clone().project(camera);
return { x: (p.x * 0.5 + 0.5) * W, y: (-p.y * 0.5 + 0.5) * H, behind: p.z > 1 };
},
viewSize() { return { w: W, h: H }; },
stage(t) { stageT = t; },
highlight(k) { applyHighlight(k || null); },
/* replay the plotter wipe on the current model (first-load draw-in) */
wipe() {
if (reduced) return;
gsap.killTweensOf(swapWipe);
swapWipe.active = true;
swapWipe.x = -80;
gsap.to(swapWipe, {
x: 85, duration: 1.1, ease: "power2.inOut",
onComplete() { swapWipe.active = false; },
});
},
set(cfg) {
const next = cfg.style && houses[cfg.style] ? cfg.style : "gable";
if (next === styleKey) return;
houses[styleKey].group.visible = false;
styleKey = next;
houses[next].group.visible = true;
applyHighlight(null);
if (reduced) return;
/* the new model plots itself in, left to right */
gsap.killTweensOf(swapWipe);
swapWipe.active = true;
swapWipe.x = -80;
gsap.to(swapWipe, {
x: 85, duration: 0.9, ease: "power2.inOut",
onComplete() { swapWipe.active = false; },
});
},
};
}
+256
View File
@@ -0,0 +1,256 @@
import * as THREE from "three";
function canvasTex(size, draw, repeatX = 1, repeatY = 1) {
const c = document.createElement("canvas");
c.width = c.height = size;
draw(c.getContext("2d"), size);
const t = new THREE.CanvasTexture(c);
t.wrapS = t.wrapT = THREE.RepeatWrapping;
t.repeat.set(repeatX, repeatY);
t.colorSpace = THREE.SRGBColorSpace;
t.anisotropy = 8;
return t;
}
function rand(seed) {
let s = seed;
return () => {
s = (s * 1103515245 + 12345) & 0x7fffffff;
return s / 0x7fffffff;
};
}
/* asphalt shingle courses with staggered tabs */
function drawShingles(g, S, base, seed = 7, weather = 0) {
const r = rand(seed);
g.fillStyle = base;
g.fillRect(0, 0, S, S);
const course = S / 10;
for (let row = 0; row < 10; row++) {
const y = row * course;
const off = (row % 2) * (course * 1.1);
for (let x = -1; x < 8; x++) {
const w = course * 2.2;
const px = x * w + off;
const shade = 0.82 + r() * 0.3 - weather * r() * 0.25;
g.fillStyle = `rgba(0,0,0,${Math.max(0, 1 - shade) * 0.9})`;
g.fillRect(px, y, w - 1.5, course - 1.5);
if (r() < 0.12 * (1 + weather * 3)) {
g.fillStyle = `rgba(255,255,255,${0.04 + r() * 0.05})`;
g.fillRect(px, y, w - 1.5, course - 1.5);
}
}
g.fillStyle = "rgba(0,0,0,.5)";
g.fillRect(0, y + course - 1.5, S, 1.5);
}
}
/* hail damage overlay: bruise blotches + torn patches */
function drawStormShingles(g, S) {
drawShingles(g, S, "#4c4a45", 11, 1);
const r = rand(41);
for (let i = 0; i < 7; i++) {
const x = r() * S, y = r() * S, rad = 18 + r() * 46;
const gr = g.createRadialGradient(x, y, 2, x, y, rad);
gr.addColorStop(0, "rgba(140,60,20,.5)");
gr.addColorStop(0.6, "rgba(120,50,25,.22)");
gr.addColorStop(1, "rgba(0,0,0,0)");
g.fillStyle = gr;
g.fillRect(x - rad, y - rad, rad * 2, rad * 2);
}
for (let i = 0; i < 3; i++) {
const x = r() * S * 0.8, y = r() * S * 0.8;
g.fillStyle = "rgba(15,14,12,.85)";
g.fillRect(x, y, 26 + r() * 30, 12 + r() * 10);
}
}
function drawClapboard(g, S, base, shadow) {
const board = S / 12;
for (let i = 0; i < 12; i++) {
const y = i * board;
const gr = g.createLinearGradient(0, y, 0, y + board);
gr.addColorStop(0, base);
gr.addColorStop(0.85, base);
gr.addColorStop(1, shadow);
g.fillStyle = gr;
g.fillRect(0, y, S, board);
}
const r = rand(5);
g.fillStyle = "rgba(0,0,0,.05)";
for (let i = 0; i < 40; i++) g.fillRect(r() * S, r() * S, 1.5, 4 + r() * 8);
}
function drawBrick(g, S) {
g.fillStyle = "#8a8078";
g.fillRect(0, 0, S, S);
const bh = S / 10, bw = S / 4;
const r = rand(23);
for (let row = 0; row < 10; row++) {
const off = (row % 2) * bw * 0.5;
for (let col = -1; col < 5; col++) {
const tone = 96 + r() * 46;
g.fillStyle = `rgb(${tone + 20},${tone * 0.62},${tone * 0.5})`;
g.fillRect(col * bw + off + 2, row * bh + 2, bw - 4, bh - 4);
}
}
}
function drawConcrete(g, S, tone = 168) {
g.fillStyle = `rgb(${tone},${tone},${tone - 4})`;
g.fillRect(0, 0, S, S);
const r = rand(3);
for (let i = 0; i < 900; i++) {
const v = r();
g.fillStyle = v > 0.5 ? "rgba(255,255,255,.05)" : "rgba(0,0,0,.06)";
g.fillRect(r() * S, r() * S, 1 + r() * 2, 1 + r() * 2);
}
}
function drawAsphalt(g, S) {
g.fillStyle = "#292b2a";
g.fillRect(0, 0, S, S);
const r = rand(17);
for (let i = 0; i < 1200; i++) {
const v = r();
g.fillStyle = v > 0.55 ? "rgba(255,255,255,.05)" : "rgba(0,0,0,.12)";
g.fillRect(r() * S, r() * S, 1 + r() * 2, 1 + r() * 2);
}
}
/* grass detail and the radial alpha falloff baked into ONE texture (repeat 1x1),
because standard materials share the map's UV transform across texture slots */
function drawGrassDisc(g, S) {
g.fillStyle = "#242d1b";
g.fillRect(0, 0, S, S);
const r = rand(9);
for (let i = 0; i < 26000; i++) {
const v = r();
const c = v > 0.66 ? "rgba(58,74,40,.5)" : v > 0.33 ? "rgba(34,44,26,.55)" : "rgba(20,26,16,.5)";
g.fillStyle = c;
g.fillRect(r() * S, r() * S, 1 + r() * 2.4, 1 + r() * 2);
}
/* mow lines */
g.fillStyle = "rgba(255,255,255,.016)";
for (let i = 0; i < 14; i++) g.fillRect(0, i * (S / 14), S, S / 28);
/* carve the radial fade into the alpha channel */
g.globalCompositeOperation = "destination-in";
const gr = g.createRadialGradient(S / 2, S / 2, S * 0.18, S / 2, S / 2, S * 0.5);
gr.addColorStop(0, "rgba(0,0,0,1)");
gr.addColorStop(0.5, "rgba(0,0,0,1)");
gr.addColorStop(0.78, "rgba(0,0,0,.45)");
gr.addColorStop(0.98, "rgba(0,0,0,0)");
g.fillStyle = gr;
g.fillRect(0, 0, S, S);
g.globalCompositeOperation = "source-over";
}
/* street strip: asphalt with lane dashes, alpha-faded at both ends */
export function drawRoad(g, S) {
drawAsphalt(g, S);
g.fillStyle = "rgba(210,200,160,.5)";
for (let i = 0; i < 9; i++) g.fillRect(i * (S / 9) + S / 36, S * 0.485, S / 18, S * 0.03);
g.fillStyle = "rgba(220,220,220,.28)";
g.fillRect(0, S * 0.06, S, S * 0.016);
g.fillRect(0, S * 0.924, S, S * 0.016);
g.globalCompositeOperation = "destination-in";
const gr = g.createLinearGradient(0, 0, S, 0);
gr.addColorStop(0, "rgba(0,0,0,0)");
gr.addColorStop(0.18, "rgba(0,0,0,1)");
gr.addColorStop(0.82, "rgba(0,0,0,1)");
gr.addColorStop(1, "rgba(0,0,0,0)");
g.fillStyle = gr;
g.fillRect(0, 0, S, S);
g.globalCompositeOperation = "source-over";
}
/* soft dark contact blob that grounds whatever stands on it */
export function drawContact(g, S) {
const gr = g.createRadialGradient(S / 2, S / 2, S * 0.05, S / 2, S / 2, S * 0.5);
gr.addColorStop(0, "rgba(0,0,0,.62)");
gr.addColorStop(0.55, "rgba(0,0,0,.34)");
gr.addColorStop(1, "rgba(0,0,0,0)");
g.fillStyle = gr;
g.fillRect(0, 0, S, S);
}
function drawGaragePanels(g, S) {
g.fillStyle = "#d8d6d0";
g.fillRect(0, 0, S, S);
const ph = S / 4;
for (let i = 0; i < 4; i++) {
const y = i * ph;
const gr = g.createLinearGradient(0, y, 0, y + ph);
gr.addColorStop(0, "rgba(0,0,0,.22)");
gr.addColorStop(0.14, "rgba(255,255,255,.16)");
gr.addColorStop(0.85, "rgba(0,0,0,.05)");
gr.addColorStop(1, "rgba(0,0,0,.34)");
g.fillStyle = gr;
g.fillRect(0, y, S, ph);
}
}
/* soft elliptical glow for the LiDAR sheet: fades before every plane edge */
function drawScanBand(g, S) {
const gr = g.createRadialGradient(S / 2, S / 2, S * 0.05, S / 2, S / 2, S * 0.5);
gr.addColorStop(0, "rgba(255,214,90,.85)");
gr.addColorStop(0.45, "rgba(255,184,0,.34)");
gr.addColorStop(1, "rgba(255,184,0,0)");
g.fillStyle = gr;
g.fillRect(0, 0, S, S);
}
export function makeMaterials() {
const std = (o) => new THREE.MeshStandardMaterial(o);
const shingleTex = canvasTex(256, (g, s) => drawShingles(g, s, "#2b2e32", 7), 6, 3);
const shingleWarmTex = canvasTex(256, (g, s) => drawShingles(g, s, "#352e26", 19), 6, 3);
const stormTex = canvasTex(512, (g, s) => drawStormShingles(g, s), 3, 1.6);
return {
shingle: std({ map: shingleTex, roughness: 0.92, metalness: 0.02 }),
shingleWarm: std({ map: shingleWarmTex, roughness: 0.92, metalness: 0.02 }),
shingleStorm: std({ map: stormTex, roughness: 0.95, metalness: 0.02 }),
sidingWhite: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#c9c6bd", "#8f8d85"), 3, 3), roughness: 0.85 }),
sidingBeige: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#b3a58c", "#7d7360"), 3, 3), roughness: 0.85 }),
sidingGray: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#9aa0a2", "#6a6f71"), 3, 3), roughness: 0.85 }),
trim: std({ color: 0xe8e6df, roughness: 0.6 }),
fascia: std({ color: 0xdad7cf, roughness: 0.65 }),
doorRed: std({ color: 0x7e2f26, roughness: 0.5 }),
doorBlue: std({ color: 0x2c3f4d, roughness: 0.5 }),
glass: std({ color: 0x0e1a22, roughness: 0.08, metalness: 0.85, envMapIntensity: 1.4 }),
garage: std({ map: canvasTex(256, drawGaragePanels, 1, 1), roughness: 0.6 }),
brick: std({ map: canvasTex(256, drawBrick, 1.4, 2.4), roughness: 0.95 }),
concrete: std({ map: canvasTex(256, (g, s) => drawConcrete(g, s), 2, 2), roughness: 0.95 }),
asphalt: std({ map: canvasTex(256, drawAsphalt, 2, 3), roughness: 0.98 }),
grass: std({
map: canvasTex(2048, drawGrassDisc),
transparent: true,
roughness: 1,
depthWrite: false,
}),
soil: std({ color: 0x2b2822, roughness: 1 }),
trunk: std({ color: 0x3c2f21, roughness: 1 }),
foliage: std({ color: 0x33431f, roughness: 1, flatShading: true }),
foliage2: std({ color: 0x415229, roughness: 1, flatShading: true }),
contact: new THREE.MeshBasicMaterial({
map: canvasTex(512, drawContact),
transparent: true,
depthWrite: false,
}),
road: std({ map: canvasTex(512, drawRoad), transparent: true, roughness: 0.98, depthWrite: false }),
tarp: std({ color: 0x27506e, roughness: 0.55, side: THREE.DoubleSide }),
porchRoof: std({ color: 0x3a3d40, roughness: 0.7, metalness: 0.25 }),
hlAmber: new THREE.MeshBasicMaterial({ color: 0xffb800, transparent: true, opacity: 0.95 }),
hlFace: new THREE.MeshBasicMaterial({ color: 0xffc233, transparent: true, opacity: 0.62, side: THREE.DoubleSide, depthWrite: false }),
impact: std({ color: 0xff5533, emissive: 0xd23c14, emissiveIntensity: 0.9, roughness: 0.6 }),
scan: new THREE.MeshBasicMaterial({
map: canvasTex(256, drawScanBand),
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide,
}),
};
}
/* alpha-fade note: the grass disc uses alphaMap + depthWrite:false so the lawn
dissolves radially into the page background instead of ending in a hard edge */
+1777
View File
File diff suppressed because it is too large Load Diff
+372
View File
@@ -0,0 +1,372 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Terms of Service - LynkedUp Pro</title>
<meta name="description" content="The agreement between you and LynkedUp Pro for using the platform: acceptance, billing, data ownership, acceptable use, and liability.">
<meta property="og:title" content="Terms of Service - LynkedUp Pro">
<meta property="og:description" content="The agreement between you and LynkedUp Pro for using the platform: acceptance, billing, data ownership, acceptable use, and liability.">
<meta property="og:type" content="website">
<meta property="og:image" content="../og.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Terms of Service - LynkedUp Pro">
<meta name="twitter:description" content="The agreement between you and LynkedUp Pro for using the platform: acceptance, billing, data ownership, acceptable use, and liability.">
<meta name="twitter:image" content="../og.jpg">
<meta name="theme-color" content="#0B0D10">
<meta name="author" content="Sarthak">
<!-- Created by Sarthak -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8.8 6 3.6 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%232EC5DD' stroke-width='1.8'/%3E%3Cpath d='M15.2 6 10 9v6l5.2 3 5.2-3V9z' fill='none' stroke='%23FFB300' stroke-width='1.8'/%3E%3C/svg%3E">
</head>
<body>
<header>
<div class="wrap nav">
<a class="logo" href="../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#5BD9EE"/><stop offset="1" stop-color="#1B7E9B"/>
</linearGradient>
</defs>
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="url(#lgA)" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="../index.html#product">Product</a>
<a href="../index.html#chaos">Why it works</a>
<a href="../index.html#intel">Intelligence</a>
<a href="../index.html#pricing">Pricing</a>
<a href="../index.html#founder">About</a>
<a href="../blog/">Blog</a>
<a href="../faq.html">FAQ</a>
</nav>
<a class="btn solid" href="../index.html#book">Book demo</a>
<button class="menu-btn" id="menuBtn" aria-label="Menu" aria-expanded="false"><i></i><i></i><i></i></button>
</div>
</header>
<div class="mprog" aria-hidden="true"><i id="mprogBar"></i></div>
<nav class="rail" id="rail" aria-label="Section index">
<div class="rail-track" id="railTrack">
<div class="rail-carriage" id="railCar"><span class="rail-read" id="railRead"></span></div>
</div>
</nav>
<main id="top">
<!-- ============ TERMS INTRO ============ -->
<div class="page-hero" id="intro">
<div class="wrap">
<span class="sheet num" aria-hidden="true">01</span>
<div class="dim-rule reveal" aria-hidden="true"><i class="l"></i><span>SEC 01 &middot; LEGAL</span><i class="r"></i></div>
<div class="sec-head reveal">
<span class="tag">Legal</span>
<h1>Terms of <em>Service.</em></h1>
<p>The agreement between you and LynkedUp Pro for using the platform. Written to be readable &mdash; skip to the section you need with the index below.</p>
<p class="tag terms-eff">EFFECTIVE JAN 1, 2026 &middot; LAST UPDATED JUL 14, 2026</p>
</div>
<div class="faq-quicknav reveal" role="group" aria-label="Jump to section">
<a class="chip" href="#acceptance">Acceptance</a>
<a class="chip" href="#use">Platform use</a>
<a class="chip" href="#accounts">Accounts</a>
<a class="chip" href="#billing">Billing</a>
<a class="chip" href="#data">Data</a>
<a class="chip" href="#conduct">Acceptable use</a>
<a class="chip" href="#disclaimers">Liability</a>
<a class="chip" href="#termination">Termination</a>
<a class="chip" href="#changes">Changes</a>
<a class="chip" href="#contact">Contact</a>
</div>
</div>
</div>
<!-- ============ CLAUSES ============ -->
<section id="clauses">
<div class="wrap">
<div class="terms-grid">
<div class="terms-card reveal" id="acceptance" data-rail="01 · ACCEPTANCE">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M4.5 12.5 9.5 17.5 19.5 6.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<div><span class="tc-num">01</span><h2>Acceptance of Terms</h2></div>
</div>
<p>By creating an account, accessing, or using LynkedUp Pro (the "Platform"), you agree to be bound by these Terms of Service and our Privacy Policy. If you're accepting on behalf of a company, you confirm you have authority to bind that company to these terms.</p>
<p>If you don't agree with any part of these terms, don't use the Platform. We may update these terms from time to time; continued use after an update means you accept the revised terms.</p>
</div>
<div class="terms-card reveal" id="use" data-rail="02 · PLATFORM USE">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><rect x="3" y="5" width="18" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M9 20h6M12 17v3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<div><span class="tc-num">02</span><h2>Use of the Platform</h2></div>
</div>
<p>LynkedUp Pro grants you a limited, non-exclusive, non-transferable license to access and use the Platform for your internal business operations, subject to these terms and your subscription tier.</p>
<ul>
<li>You may not resell, sublicense, or white-label the Platform without written permission.</li>
<li>You may not reverse-engineer, decompile, or attempt to extract the underlying source code.</li>
<li>Features, integrations, and AI tools may change, expand, or be deprecated as the product evolves; we'll provide reasonable notice for changes that remove core functionality.</li>
</ul>
</div>
<div class="terms-card reveal" id="accounts" data-rail="03 · ACCOUNTS">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><circle cx="12" cy="9" r="3.3" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M5.5 20c1.2-3.7 4-5.5 6.5-5.5s5.3 1.8 6.5 5.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<div><span class="tc-num">03</span><h2>Accounts &amp; Eligibility</h2></div>
</div>
<p>You must be at least 18 years old and able to form a binding contract to use LynkedUp Pro. You're responsible for the accuracy of the information you provide and for keeping your login credentials secure.</p>
<p>Any activity that happens under your account is your responsibility, whether performed by you, an employee, or a subcontractor you've granted access to. Notify us immediately if you suspect unauthorized access.</p>
</div>
<div class="terms-card reveal" id="billing" data-rail="04 · BILLING">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><rect x="3" y="6" width="18" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3 10h18" stroke="currentColor" stroke-width="1.6"/></svg></span>
<div><span class="tc-num">04</span><h2>Subscriptions &amp; Billing</h2></div>
</div>
<p>Standard per-seat plans (Starter, Pro) are currently on hold; new accounts are onboarded through the Founders Lifetime License, a one-time fee that reserves lifetime access to the core platform. Details and current pricing are listed on our <a href="../index.html#pricing">pricing page</a>.</p>
<p>Where recurring billing does apply (e.g. add-ons or future per-seat plans), charges renew automatically on your billing cycle unless cancelled beforehand. Fees are non-refundable except where required by law or explicitly stated at time of purchase.</p>
</div>
<div class="terms-card reveal" id="data" data-rail="05 · DATA">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M12 3.5 19 6.5v5.5c0 4.6-3 7.6-7 8.5-4-.9-7-3.9-7-8.5V6.5Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg></span>
<div><span class="tc-num">05</span><h2>Data &amp; Content Ownership</h2></div>
</div>
<p>You own the data you put into LynkedUp Pro: your job files, photos, scans, customer records, and documents. We don't sell or share your job records with third parties, and every export leaves in standard formats you can take with you at any time.</p>
<p>We may use de-identified, aggregated data to improve estimation models and product features. This never includes customer contact details or identifiable job records.</p>
</div>
<div class="terms-card reveal" id="conduct" data-rail="06 · ACCEPTABLE USE">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M12 4 21 19H3Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M12 10v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="12" cy="16.4" r=".9" fill="currentColor"/></svg></span>
<div><span class="tc-num">06</span><h2>Acceptable Use</h2></div>
</div>
<p>You agree not to use the Platform to:</p>
<ul>
<li>Upload unlawful, fraudulent, or misleading content, including falsified inspection or claim evidence.</li>
<li>Interfere with or disrupt the integrity or performance of the Platform, its APIs, or connected integrations.</li>
<li>Attempt to access another customer's account, job data, or workspace without authorization.</li>
<li>Use the Platform in a way that violates any applicable local, state, or federal law.</li>
</ul>
</div>
<div class="terms-card reveal" id="disclaimers" data-rail="07 · LIABILITY">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M12 3v17M7 6h10M4.5 9 7 6l2.5 3M14.5 9 17 6l2.5 3" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 9c0 2 1.3 3.4 3 3.4S10 11 10 9M14 9c0 2 1.3 3.4 3 3.4S20 11 20 9" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 20h8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<div><span class="tc-num">07</span><h2>Disclaimers &amp; Limitation of Liability</h2></div>
</div>
<p>The Platform, including AI-generated estimates, damage assessments, and automation, is provided "as is." AI outputs are decision support, not a replacement for professional judgment &mdash; a qualified human should review estimates, claims, and safety-critical decisions before acting on them.</p>
<p>To the fullest extent permitted by law, LynkedUp Pro is not liable for indirect, incidental, or consequential damages arising from use of the Platform, including denied claims, missed jobs, or business interruption, beyond the amount paid for the service in the preceding 12 months.</p>
</div>
<div class="terms-card reveal" id="termination" data-rail="08 · TERMINATION">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M12 4v7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M7 6.5a7 7 0 1 0 10 0" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg></span>
<div><span class="tc-num">08</span><h2>Termination</h2></div>
</div>
<p>You may cancel your account at any time; your data remains exportable for 30 days after cancellation. We may suspend or terminate accounts that violate these terms, with notice where practical, or immediately in cases of fraud, security risk, or non-payment.</p>
<p>Founders Lifetime License holders retain lifetime access to core features under this agreement as long as the account remains in good standing and in compliance with these terms.</p>
</div>
<div class="terms-card reveal" id="changes" data-rail="09 · CHANGES">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><path d="M4 12a8 8 0 0 1 13.5-5.8M20 12a8 8 0 0 1-13.5 5.8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M17 3v4h-4M7 21v-4h4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<div><span class="tc-num">09</span><h2>Changes to These Terms</h2></div>
</div>
<p>We may revise these terms as the product and business evolve. Material changes will be posted here with an updated "last updated" date, and where required, communicated directly to account owners. Continued use of the Platform after changes take effect constitutes acceptance.</p>
</div>
<div class="terms-card reveal" id="contact" data-rail="10 · CONTACT">
<div class="tc-head">
<span class="tc-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="19" height="19"><rect x="3" y="5.5" width="18" height="13" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M4 7l8 6 8-6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<div><span class="tc-num">10</span><h2>Governing Law &amp; Contact</h2></div>
</div>
<p>These terms are governed by the laws of the State of Texas, without regard to conflict-of-law principles. Any disputes will be resolved in the state or federal courts located in Dallas County, Texas.</p>
<p>Questions about these terms? Reach us at <a href="mailto:Info@LynkedUpPro.com">Info@LynkedUpPro.com</a> or (866) 259-6533.</p>
</div>
</div>
<div class="founder-card terms-cta reveal">
<span class="stamp-tag ok">&#10003; QUESTIONS ABOUT THESE TERMS?</span>
<h2>Talk to a <em>real person.</em></h2>
<p>Legal questions, data requests, or account-specific terms &mdash; our team can walk you through any section above.</p>
<div class="cta-alt">
<a class="btn solid" href="../index.html#book">Book a demo</a>
<a class="btn" href="tel:8662596533">Call (866) 259-6533</a>
</div>
</div>
</div>
</section>
</main>
<footer>
<svg class="skyline2" viewBox="0 0 1440 150" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<g class="sk-ln">
<line x1="0" y1="148" x2="1440" y2="148"/>
<g>
<rect x="52" y="78" width="86" height="70"/>
<polygon points="47,78 95,52 143,78"/>
<rect x="88" y="122" width="16" height="26"/>
<rect class="w-on" x="62" y="88" width="10" height="12"/>
<rect class="w-off" x="80" y="88" width="10" height="12"/>
<rect class="w-on w-tw" x="112" y="88" width="10" height="12"/>
<rect class="w-off" x="62" y="112" width="10" height="12"/>
<rect class="w-on" x="112" y="112" width="10" height="12"/>
</g>
<g>
<rect x="186" y="104" width="112" height="44"/>
<polygon points="180,104 242,80 304,104"/>
<rect x="222" y="68" width="12" height="24"/>
<rect class="w-off" x="198" y="114" width="11" height="12"/>
<rect class="w-on" x="222" y="114" width="11" height="12"/>
<rect class="w-off" x="246" y="114" width="11" height="12"/>
<rect x="272" y="118" width="14" height="30"/>
</g>
<g>
<line x1="340" y1="148" x2="340" y2="102"/>
<line x1="340" y1="102" x2="352" y2="102"/>
<circle class="lamp" cx="354" cy="104" r="3"/>
<circle class="lamp-halo" cx="354" cy="106" r="10"/>
</g>
<g>
<rect x="392" y="66" width="64" height="82"/>
<polygon points="388,66 424,44 460,66"/>
<rect class="w-on" x="402" y="76" width="10" height="12"/>
<rect class="w-off" x="432" y="76" width="10" height="12"/>
<rect class="w-off" x="402" y="102" width="10" height="12"/>
<rect class="w-on w-tw2" x="432" y="102" width="10" height="12"/>
<rect x="416" y="124" width="15" height="24"/>
</g>
<g>
<rect x="512" y="96" width="96" height="52"/>
<polygon points="506,96 534,74 586,74 614,96"/>
<rect class="w-on" x="524" y="108" width="11" height="12"/>
<rect class="w-on" x="548" y="108" width="11" height="12"/>
<rect class="w-off" x="572" y="108" width="11" height="12"/>
<rect x="616" y="112" width="52" height="36"/>
<polygon points="612,112 642,96 672,112"/>
<rect class="w-off" x="630" y="122" width="24" height="20"/>
</g>
<g>
<line x1="712" y1="148" x2="712" y2="120"/>
<circle cx="712" cy="106" r="16"/>
<circle cx="700" cy="116" r="10"/>
</g>
<g>
<rect x="762" y="72" width="110" height="76"/>
<polygon points="756,72 817,46 878,72"/>
<line x1="758" y1="120" x2="876" y2="120"/>
<line x1="770" y1="120" x2="770" y2="148"/>
<line x1="817" y1="120" x2="817" y2="148"/>
<line x1="864" y1="120" x2="864" y2="148"/>
<rect class="w-on" x="774" y="82" width="10" height="12"/>
<rect class="w-on" x="798" y="82" width="10" height="12"/>
<rect class="w-off" x="826" y="82" width="10" height="12"/>
<rect class="w-on" x="850" y="82" width="10" height="12"/>
<rect class="w-off" x="774" y="128" width="10" height="12"/>
<rect class="w-on w-tw" x="850" y="128" width="10" height="12"/>
</g>
<g>
<rect x="918" y="116" width="44" height="32"/>
<polygon points="914,116 940,102 966,116"/>
<rect class="w-off" x="932" y="124" width="16" height="16"/>
</g>
<g>
<line x1="1006" y1="148" x2="1006" y2="102"/>
<line x1="1006" y1="102" x2="1018" y2="102"/>
<circle class="lamp" cx="1020" cy="104" r="3"/>
<circle class="lamp-halo" cx="1020" cy="106" r="10"/>
</g>
<g>
<rect x="1058" y="92" width="84" height="56"/>
<polygon points="1053,92 1100,66 1147,92"/>
<rect class="w-on" x="1070" y="102" width="10" height="12"/>
<rect class="w-off" x="1094" y="102" width="10" height="12"/>
<rect class="w-on" x="1118" y="102" width="10" height="12"/>
<rect x="1094" y="124" width="14" height="24"/>
</g>
<g>
<rect x="1198" y="80" width="92" height="68"/>
<polygon points="1192,80 1244,54 1296,80"/>
<rect x="1258" y="42" width="12" height="26"/>
<rect class="w-off" x="1210" y="92" width="10" height="12"/>
<rect class="w-on w-tw2" x="1234" y="92" width="10" height="12"/>
<rect class="w-on" x="1258" y="92" width="10" height="12"/>
<rect class="w-off" x="1210" y="118" width="10" height="12"/>
<rect class="w-on" x="1258" y="118" width="10" height="12"/>
</g>
<g>
<line x1="1352" y1="148" x2="1352" y2="118"/>
<circle cx="1352" cy="102" r="18"/>
<circle cx="1368" cy="114" r="11"/>
</g>
</g>
</svg>
<div class="wrap">
<div class="foot-glass">
<div class="foot-grid">
<div class="foot-col brand">
<a class="logo" href="../index.html" aria-label="LynkedUp Pro home">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M8.8 5.6 3.4 8.75v6.5L8.8 18.4l5.4-3.15v-6.5Z" stroke="#17607A" stroke-width="1.7"/>
<path d="M15.2 5.6 9.8 8.75v6.5l5.4 3.15 5.4-3.15v-6.5Z" stroke="#2EC5DD" stroke-width="1.7"/>
</svg>
<span>LynkedUp&nbsp;<b>Pro</b></span>
</a>
<p class="foot-tag">The construction CRM that does the paperwork.</p>
<span class="tag">16990 Dallas Pkwy, Suite 206<br>Dallas, TX 75248<br>(866) 259-6533<br>Info@LynkedUpPro.com</span>
<div class="foot-social" aria-label="Social links">
<a href="../index.html" aria-label="Instagram"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="3.5" width="17" height="17" rx="4.5" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="17.2" cy="6.8" r="1.2" fill="currentColor"/></svg></a>
<a href="../index.html" aria-label="X"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5 19.5 19.5M19.5 4.5l-15 15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></a>
<a href="../index.html" aria-label="YouTube"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="12" rx="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10.4 9.4v5.2l4.6-2.6Z" fill="currentColor"/></svg></a>
</div>
</div>
<div class="foot-col">
<b>Platform</b>
<a href="../index.html#hero">Instant estimate</a>
<a href="../index.html#intel">Intelligence map</a>
<a href="../index.html#product">For contractors</a>
<a href="../index.html#industries">For homeowners</a>
<a href="../index.html#pricing">Pricing</a>
</div>
<div class="foot-col">
<b>Company</b>
<a href="../index.html#founder">About</a>
<a href="../blog/">Blog</a>
<a href="../index.html#chaos">Why it works</a>
<a href="../index.html#book">Book a demo</a>
<a href="../index.html#book">Careers</a>
<a href="../index.html#book">Contact</a>
</div>
<div class="foot-col">
<b>Resources &amp; legal</b>
<a href="../faq.html">FAQ</a>
<a href="../index.html">Help center</a>
<a href="../index.html">Security</a>
<a href="../index.html">Privacy policy</a>
<a href="../terms/">Terms of service</a>
<a href="../index.html">Drone flight safety</a>
</div>
</div>
<form class="foot-alerts" id="zipForm">
<div class="fa-copy">
<b>Hail alerts for your territory</b>
<span class="tag">Radar-verified swaths for your ZIP, the morning after the storm</span>
</div>
<div class="fa-row">
<input id="zipInput" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="75248" aria-label="ZIP code">
<button class="btn solid" type="submit">Watch my ZIP</button>
</div>
</form>
<div class="foot-base">
<span class="tag">&copy; 2026 LynkedUp Pro &middot; All rights reserved &middot; Powered by LynkedUp Technologies</span>
<span class="tag">Zero-trust architecture &middot; data encrypted at rest and in transit</span>
</div>
</div>
</div>
</footer>
<script type="module" src="../src/blog.js"></script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
{
"framework": "vite",
"buildCommand": "npm run build",
"outputDirectory": "dist",
"installCommand": "npm install"
}
+26
View File
@@ -0,0 +1,26 @@
import { defineConfig } from "vite";
import { fileURLToPath } from "node:url";
import path from "node:path";
const root = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
base: "./",
server: { port: 4190 },
build: {
rollupOptions: {
input: {
main: path.resolve(root, "index.html"),
faq: path.resolve(root, "faq.html"),
blog: path.resolve(root, "blog/index.html"),
blogCostOfPaperwork: path.resolve(root, "blog/cost-of-paperwork/index.html"),
blogStormChecklist: path.resolve(root, "blog/48-hour-storm-checklist/index.html"),
blogClaimsDenied: path.resolve(root, "blog/why-claims-get-denied/index.html"),
terms: path.resolve(root, "terms/index.html"),
},
output: {
manualChunks: { three: ["three"], gsap: ["gsap"] },
},
},
},
});