Static landing-page wireframe for Grand.
-
Added a first-pass one-page wireframe for the public landing page.
-
Positioning is companion-first for the older person, with family reassurance as the buyer story.
-
The wireframe uses the existing Grand product language: warm surfaces, sage/clay accents, editorial type moments, privacy by default, and no surveillance framing.
-
Added an optimized hero image from the selected Pexels option: Moe Magners photo
pexels-moe-magners-5335290.jpg, resized to 2400px wide atassets/hero-pexels-moe-magners-5335290.jpg. -
Deployed the static page with GitHub Pages from the
gh-pagesbranch underrainforestapp/grand-website; theDeploy GitHub Pagesworkflow publishesmaintogh-pagesautomatically whenmainchanges. -
Configured GitHub Pages for the canonical custom domain
www.grandeldercare.com, with DNSimple records pointingwwwtorainforestapp.github.ioand the apex domain to GitHub Pages A/AAAA records. GitHub is issuing a certificate for bothwww.grandeldercare.comandgrandeldercare.com. -
Refined the landing-page copy after review feedback, including more specific music examples, clearer reminder language, and replacing the abstract product preview with concrete Grace app ideas.
-
Reframed the landing page around the broader Grand system: Grace is the smart speaker companion, and optional Grand Satellites add home tracking.
-
Added a "How Grand works" section that explains Grace as the smart speaker and Grand Satellites as the optional home-tracking add-on.
-
Added a Grand satellites/home-tracking section focused on daily rhythm, routine changes, and filtered emergency-like alerts without cameras or live feeds.
-
Split use cases into "Ask Grace" and "Grand notices" so companion moments and home-tracking moments can sit together without blurring their roles.
-
Replaced the abstract satellite floor-plan with resized real Grand iOS app screenshots from
../grand-ios/docs/screenshots:ios-home.pngandpr1-settings-index.png. -
Simplified the hero headline and strapline so first-time readers understand the promise without needing prior context for Grand, Grace, or satellites.
-
Rewrote the hero to lead with reassurance instead of the two-part independence/worry construction. Why: "You'll know she's okay." states the outcome the buyer actually wants in one breath, and the shorter strapline ("A few discreet sensors to flag when something's off — no cameras, no wearables, nothing to charge.") drops the "small hub" mechanism detail that the "How Grand works" section already covers. Also swapped the hero CTAs so "See how Grand works" is now primary (filled) and reordered ahead of the secondary "Be first to know" outline button, because an unconvinced first-time visitor needs to understand the product before committing to the waitlist. Removed the "Care at home" hero eyebrow so nothing competes with the headline, added
text-wrap: balanceon#hero-titleso the two-line headline doesn't orphan "okay." on its own line, and raised the hero copy's bottom padding (clamp(42px, 9vw, 140px)) so the shorter block sits optically centered on tall desktop viewports instead of pinned to the bottom edge. -
Wired the waitlist form for a Google Sheets backend via Google Apps Script, including client-side email validation, status messaging, and browser/user-agent metadata capture.
-
Repositioned the entire page from companion-first to peace-of-mind / dignity-first, aimed squarely at the adult child (the buyer). This supersedes the companion-first framing above. Why: the product's MVP is a quiet, passive sensor system (a hub + sensors) that lets an adult child know their parent is okay without cameras, wearables, or anything that announces "you're old" — not a voice companion. The "Grace" companion concept and all companion features (music, reading aloud, trivia, conversation, family-message dictation) were removed because they describe a different product and were not in the MVP spec.
- "Grand satellites" renamed to "Grand sensors"; the device story is now simply "the Grand hub + Grand sensors."
- Hero now leads with the fear→relief promise. (The wording was later tightened to "You'll know she's okay." — see the hero rewrite below.)
- Added an upfront "The worry" problem section after the hero that sets up the fear and contrasts pendants/watches, call-for-help buttons, and cameras before presenting the solution. (This replaced a later "Why not a wearable, button, or camera?" comparison block, which said the same thing twice once the problem was framed up front.)
- Trust close ties privacy directly to dignity and drops conversation-era rules; no pricing and no public mention of human-in-the-loop alert review, per product decision. (The human-in-the-loop decision was later reversed — see the Grand Call Center section below.)
- Reused the existing iOS screenshots (
assets/grand-ios-home.png,assets/grand-ios-settings.png) — they already show the passive-sensing view with no "Grace" UI.
-
Added a "Grand Call Center" section (
#response) between the caregiver experience and the waitlist. Why: this reverses the earlier "no public mention of human-in-the-loop alert review" decision — the call center is now a headline differentiator, because "what actually happens in an emergency?" is the buyer's biggest pre-purchase question and the human agent + EMS escalation is the answer. The section presents the emergency flow as a four-step process (a real person calls through the hub/sensors → confirms she's safe → calls EMS if not → the family is notified throughout and can join the call), with a call-center agent photo (assets/call-center-pexels-kampus-8204317.jpg, Pexels / Kampus Production, cropped 3:4 around the agent and optimized to ~250KB). The caregiver-experience intro and the urgent signal tile were reworded to hand off to this section instead of implying an automated-only reach-out, and an "Emergency response" nav link was added. -
Reformatted "The worry" section from a three-card competitor teardown into a narrative "anxiety window" timeline plus a compact "you've probably already thought about…" strikethrough list. Why: user research (May–June interviews) showed the problem is emotional, not comparative — the single most vivid finding was the adult child who worries from the moment she wakes until it's socially acceptable to call at 8am, and the failed alternatives land as stories (the pendant on the nightstand during the fall, the button that's "a reminder you're old") rather than spec-sheet dismissals. The old equal-cards format asked visitors to evaluate product categories before feeling understood, and gave competitor categories the same visual weight as the worry itself. The timeline dramatizes a familiar morning (clay dots for the anxious beats, a sage dot for the relief beat), a payoff line bridges into "How Grand works," and the three alternatives survive as demoted one-line dismissals so the content wasn't lost.
-
Added baseline SEO and indexing plumbing: canonical URL, Open Graph and Twitter card tags, JSON-LD structured data (Organization + WebSite),
robots.txt,sitemap.xml, and a favicon set. Why: the site previously had none of this, so Google had little to work with and link previews in iMessage/Slack/social showed no image or branding. This is intentionally the crawlability baseline, not an optimization pass. See the "SEO & Indexing" section below. -
Enriched waitlist signups to qualify fit against the ICP without adding friction to the email capture. Why: the sheet previously held only an email, giving no way to tell whether a signup matches the target buyer (an anxious adult child of a single senior who lives alone) or where to launch first. Three changes:
- Passive location at signup.
script.jsfires a best-effort client-side IP geolocation lookup (https://ipapi.co/json/) on page load and attachesgeo(city/region/country/postal) to the signup payload. It is time-boxed to ~1.2s and can never block or fail a signup — if it's slow or blocked, the row just has no location. This is on top of the coarsetimezonealready captured. - Post-signup profile page (
welcome.html). The email stays the only required field; on success the homepage stores the email insessionStorage(not the URL, to avoid leaking it into referrer/pixel traffic) and redirects towelcome.html, which asks five optional questions — full name, ZIP code, why they're interested, whether the person Grand is for lives alone, and alpha-tester interest. This is progressive profiling: motivated signers answer, hesitant ones still convert. The page isnoindex. - Real conversion events. Both pixels previously fired only
PageView/PageVisit, so ad platforms couldn't see signups.script.jsnow firesfbq('track','Lead')+rdt('track','SignUp')on signup, andfbq('track','CompleteRegistration')+rdt('track','Lead')on profile completion.
- Passive location at signup.
-
Added Privacy Policy (
privacy.html) and Terms of Service (terms.html) pages and linked both from the footer ofindex.html,welcome.html, and each other. Why: the site runs Meta and Reddit ad pixels and collects waitlist emails plus optional profile data, so it needs a published privacy policy and terms — and the ad platforms require a linked privacy policy for pixel/conversion use. Both pages are boilerplate written from what we know about Grand (pre-launch home-sensor product, waitlist only, US-based call center is not yet live), using a new shared.legal-*layout instyles.css(readable 760px prose column, serif section headings, clay links) that matches the site's warm surfaces and editorial type. The privacy policy explicitly discloses that a waitlist sign-up is recorded as a conversion event and shared back to advertising partners (Meta, Reddit) so campaigns can be measured — the specific fact the team wanted stated. The legal pages deliberately omit the Meta/Reddit pixel snippets (no need to fire ad tracking on the privacy/terms pages themselves) and are indexable (added tositemap.xml). One placeholder to confirm with counsel: governing law is set to Delaware. All contact addresses site-wide usehello@grandeldercare.com— amailto:typo (grandelderare.com, missing a "c") in theindex.htmlandwelcome.htmlfooters was corrected as part of this change. -
Rebuilt the site footer: a top row with a "Contact us" label +
hello@grandeldercare.commailto:link on the left and the four nav links as a single right-aligned column, above a bottom bar with thegrand.wordmark logo (assets/grand-logo.png) on the bottom-left and the copyright on the bottom-right (no divider — the footer reads as one section). Why: earlier footer iterations laid the nav links out as a run-on horizontal line, then as labelled columns that still read as cluttered. This is the simplified layout the team asked for. The "Grand is not a replacement for 911 or professional medical care" disclaimer was dropped per request, and the earlier "Quiet home monitoring…" tagline was removed from the footer (it wasn't requested). The logo is the brand wordmark trimmed and made transparent (cream background removed) so it blends on the footer surface. -
Added "Grace" product-experiment subpages at
/gracecompanionand/gracephoneas lean, de-branded scaffolds, with/gracenow available as a clone of/gracecompanion. Why: we want to test separate product lines under the Grand umbrella without touching the main Grand page or linking the sites together. See the "Grace Product Subpages" section below. The main Grand page (index.html) and all its supporting files were left completely untouched. -
Consolidated the Grace experiment to a single
/gracepage. Deleted thegracecompanion/andgracephone/folders, leaving/graceas the only Grace site. Why: we no longer need three parallel product-line experiments — one Grace page is enough to maintain and iterate on. This supersedes the "three independent sites" model above. On the next deploy thersync --deletestep drops the live/gracecompanionand/gracephoneURLs automatically; nothing else referenced them (no cross-nav, not insitemap.xml), and the root Grand site is untouched./gracekeeps its existingproduct = "gracecompanion"waitlist tag so signups continue flowing to the GraceCompanion sheet tabs (data continuity; the label is invisible to users). -
Standardized the
/grace"What Grace does" section to list format. Converted the two flowing-paragraph blocks ("Grace keeps her company", "She brings people closer") to the same.does-listbold-lead-in style as the middle block, and removed the leftover[TK - confirm in v1]marker. Why: the section mixed prose and list formatting; the founder found the scannable bold-lead-in list easier to read, so all three blocks now match. HTML-only change —.does-listwas already defined ingrace/styles.css. -
Switched the Grand alpha signup from email to phone number, on its own sheet tabs. The "Become a tester" field on
index.htmlnow collects a phone number (type="tel", 10–15 digit validation) instead of an email, and the whole Grand site is taggedproduct = "grandphone"so signups, profile answers, and analytics events route to two new tabs —Grand phone number alpha listandGrand phone number events— via the existingPRODUCT_SHEETSmechanism. Why: the team wants to reach alpha testers by phone for onboarding calls, and a clean break onto new tabs keeps phone numbers out of the historicalemailcolumn and freezes the email-eraWaitlist/Eventstabs as an archive.waitlist.gsgained aphonecolumn (appended last, so existing sheets auto-migrate with a blank column) and now identifies people by phone (digits-only match) for phone products, keeping the email contract for everything else. Deploy dependency: the Apps Script must be redeployed (Deploy → Manage deployments → New version) for phone signups to be accepted — the currently deployed version validates email server-side and would reject them. Ship the frontend and the redeploy together. -
Reworked the post-submit screen on
welcome.htmlinto an alpha-onboarding CTA. Moved the alpha-tester Calendly link out of the profile form onto the confirmation screen, then dropped the "Thank you."/"we'll be in touch" copy in favor of a single focused ask: heading "Fast track getting set up as a test user of Grand", a short explainer, and a primary "Schedule a 20-minute call with us" button (Calendly). Removed the "Back to home" button. Why: once someone finishes the profile, booking the onboarding call is the only next step worth surfacing — a confirmation-and-dead-end screen wasted the highest-intent moment. -
Fixed phone numbers landing in the sheet as
#ERROR!. Phone values start with+(e.g.+1 (781) 492-4290), and Google Sheets parses any cell beginning with+,=,-, or@as a formula, so the phone column evaluated to#ERROR!.waitlist.gsalready prefixed the value with a text-forcing apostrophe (plainTextPhone_); this adds a second guard,ensurePhoneColumnIsText_, which sets the phone column's number format to plain text (@) before every write so a raw value can never be parsed as a formula. Why belt-and-suspenders: the format guard is independent of the apostrophe and protects any future write path. Deploy dependency: this only takes effect once the Apps Script is redeployed (Deploy → Manage deployments → New version) — the error rows in the live sheet were written by the pre-fix deployment. Existing#ERROR!cells must be corrected by hand; the original number is still recoverable from that row'sraw_payloadJSON ("phone":"…"). -
Gated the "Schedule 20-minute call" link behind a fit qualifier on
welcome.html. The profile form gained two questions — the caregiver's phone type (iPhone / Android / Something else) and "Does your loved one have any pets?" (Yes / No) — and, together with the existing "does the person live alone?" question, these three now qualify the visitor. On submit, only a caregiver who uses an iPhone, whose loved one lives alone, and has no pets sees the Calendly scheduling panel; everyone else lands on a new Grace-style "You're on the list." panel (no call link). Every submission's answers are still saved either way, andscript.jstags thewaitlist_profile_submit_success/profile_completedanalytics events withqualifiedso the split is visible. Why: too many low-fit people were booking onboarding calls — the alpha needs iPhone users (the companion app is iOS-only), single seniors living alone (the core ICP), and pet-free homes (pets confound the passive motion sensing), so the call slot should be reserved for candidates who clear all three. Before/After: Before — every profile submitter saw "Schedule 20-minute call". After — only all-three qualifiers see it; the rest get a graceful waitlist dead-end.waitlist.gsgainedphone_typeandhas_petscolumns (appended last, so existing sheets auto-migrate with blank columns). Deploy dependency: redeploy the Apps Script (Deploy → Manage deployments → New version) so the two new profile answers are written to the sheet — the site otherwise works, the columns just stay blank until redeploy.- Made every profile field except "Why are you interested?" required, and moved that free-text question to the end. Full name, email, ZIP, phone type, lives-alone, and pets must all be answered before the form submits (blank → "Please complete all required fields." and focus jumps to the first empty field); email also gets a format check ("Please enter a valid email address."). The open-ended "Why are you interested in Grand?" is now the last field and the only optional one. Each required label/legend carries a clay
*(with a "* Required" note at the top of the form); the optional field keeps its "Optional" hint. Why: the qualifier and follow-up fields are the ones we act on, so we want them complete; the essay question is nice-to-have and belongs after the quick taps so it never blocks a motivated signup. - Pets lists "No" first, with no option pre-selected. Why: most target households are pet-free, so No leads; leaving it unselected keeps pets a genuine required choice (a pre-checked default would auto-pass the qualifier) — consistent with the other required questions.
- Tightened the
welcome.htmllayout so the now-longer form sits higher on the page. Cut the.profile-sectiontop/bottom padding (clamp(56px,8vw,104px)→clamp(32px,4vw,56px)), split the.profile-layoutgridgapinto a smallerrow-gap(clamp(16px,2vw,24px)) so the heading no longer floats far above the content, and trimmed the.profile-maingap to 16px. Also moved the "* Required" note out of the form and dropped itsmargin-bottom— inside the form it added its own margin on top of the 24px field gap, leaving a ~44px hole above the first field. Now the description, "* Required", and the first field sit at an even 16px. Why: adding the required questions made the form tall enough to risk pushing the first fields below the fold; pulling the whole block up and closing the intro gap keeps the top of the form visible on a typical laptop viewport without cramping it.
- Made every profile field except "Why are you interested?" required, and moved that free-text question to the end. Full name, email, ZIP, phone type, lives-alone, and pets must all be answered before the form submits (blank → "Please complete all required fields." and focus jumps to the first empty field); email also gets a format check ("Please enter a valid email address."). The open-ended "Why are you interested in Grand?" is now the last field and the only optional one. Each required label/legend carries a clay
-
Replaced the "Grand call center" content in the
#responsesection with a caregiver-decides emergency flow. The section keeps its exact layout (photo-left/copy-right two-column grid, eyebrow →<h2>→ intro → auto-numbered.response-steps), but the copy no longer describes a U.S.-based human agent who calls the parent and escalates to EMS. New eyebrow "When something's wrong", headline "You decide what happens next.", and three steps: (1) Grand pings your caregiver circle on a fall or routine change, (2) you talk to her directly through the hub and sensors, (3) you decide if emergency services are needed — if she needs help, Grand surfaces the right local emergency number for her area. This dropped the step count from four to three; the CSS counter renumbers automatically. Why: the emergency model changed — the family, not a Grand call center, is in the loop and makes the call, so the copy now puts the caregiver in control and reflects that Grand alerts the circle rather than dialing EMS itself. This supersedes the earlier "Grand Call Center" / human-in-the-loop framing described above. Before/After: Before — "The Grand call center · If the worst does happen, we're there with her.", four steps ending in a Grand agent calling EMS. After — "When something's wrong · You decide what happens next.", three steps ending in the caregiver placing the call with Grand's help. The#responseid and "Emergency response" nav label are unchanged.- Swapped the section photo from the call-center agents (
call-center-pexels-kampus-8204317*.jpg, now deleted) to a caregiver at home checking her phone —assets/caregiver-phone-pexels-kaboompics-6135183.jpg(Pexels / Kaboompics, photo #6135183). Why: the old image showed a call center, which the new copy no longer describes; a caregiver looking at her phone matches the "Grand pings your circle / you decide" flow. Same responsive setup as before: desktop 3:4 crop (1200×1600, ~146KB) framed on the subject, with<picture>mobile sources at ≤560px — a tighter 4:3 crop at-mobile.jpg(340w) and-mobile-2x.jpg(680w). Thealttext was updated to match.
- Swapped the section photo from the call-center agents (
-
Refreshed the homepage share preview (thumbnail + description). The card previously used a tight portrait crop of a stock headshot and a description built around the old “no cameras, nothing to charge” framing. Why: the crop showed only a face — nothing about the product — so a shared link communicated “some elder-care thing” and nothing more. The new image is a wider shot of the same woman in her living room with a Grand sensor plugged into the wall outlet behind her, so the preview shows the product in situ, and the copy now leads with detection (“both acute events like falls and subtle changes in her routine”) rather than only what Grand isn't.
- Published at a new filename (
og-image-v2.jpg) instead of overwritingog-image.jpg. Why: Facebook, LinkedIn, Slack, and iMessage cache share images by URL, often for weeks. Reusing the old path would have kept serving the old thumbnail to every platform that had already scraped the site; a new URL forces a fresh fetch. It also leaves the Grace subpage's existing card untouched, since it still points at the old file. - Then aligned
<title>and<meta name="description">to the same copy. Why: leaving them on the old wording meant the search snippet and the share card described the product two different ways.<title>keeps the establishedGrand · …brand prefix (38 chars, inside Google's ~60 limit) rather than matchingog:titleexactly —og:site_namealready renders “Grand” as a separate line on the share card, but a search result has no such affordance, so dropping the brand there would cost brand-query recognition for nothing. - The search description is a deliberately shorter variant of the share description (147 vs 178 chars). Why: Google truncates at ~155–160, and the full sentence got cut mid-word at “No c…”, losing “No cameras, no wearables.” — the sharpest differentiator in the line. Dropping the “so you can be sure she’s safe” clause fits the whole thought in the snippet. The share card keeps the longer version because those platforms allow ~300 chars and the reassurance clause earns its space there.
- The
OrganizationJSON-LDdescriptionwas updated to the short variant too, replacing the old “quiet home sensor system” sentence. It feeds brand/knowledge-panel surfaces where the same length pressure applies. Note this string lives inside a<script>tag, so it uses a literal’— HTML entities like’are not decoded there and would render as raw text. - Three description strings now exist by design: the long one on
og:description(share cards), and the short one shared by<meta name="description">and the JSON-LD (search surfaces). The visible hero and “The worry” copy still use the older “nothing to charge” framing — out of scope for this pass.
- Published at a new filename (
-
Replaced the share thumbnail again with a branded, headlined version (
assets/og-image-v3.jpg). Same photograph and framing asog-image-v2.jpg, but the delivered art now has thegrand ·wordmark and the headline “You'll know she's okay” composited into the blank wall on the left. Why: the v2 card carried no brand mark and no words of its own — in a feed or a group chat it had to borrow all of its meaning from theog:titleline underneath, which several surfaces truncate or de-emphasise. Baking the promise into the pixels means the card says what Grand does even when the text around it is clipped. Before/After: Before — photo only, wall empty on the left. After — same photo with wordmark + two-line headline in that empty space.og:image:altwas rewritten to describe the text as well as the scene, since the alt string is what screen readers get instead of the baked-in words.- New filename again (
-v3) rather than overwriting-v2. Why: the same URL-keyed caching that motivated the v1→v2 rename applies here;og-image-v2.jpghas been live and scraped since the previous pass. - Kept
og:titleas “Know your aging parent is okay” rather than matching the image headline verbatim. Why: the title also has to work as a standalone line on cards that don't render the image at all, where “You'll know she's okay” has no subject. The two read as a pair when both show.
- New filename again (
-
Softened the waitlist launch-date copy from a fixed season to a relative timeframe. The
.waitlist-noteabove the phone sign-up onindex.htmlnow reads “Grand will officially launch to the public later this year, but we are accepting prototype testers today (completely free). Sign up to learn more.” Why: the public launch date is not firm, and “Fall 2026” had to be re-edited every time it moved — a relative timeframe stops the page from advertising a specific season we may miss, while keeping the prototype-tester ask (the only thing the section actually converts on) unchanged. Before/After: Before — “Grand will officially launch to the public Fall 2026, but we are…”. After — “Grand will officially launch to the public later this year, but we are…”. Copy-only change; the eyebrow, headline, form, and validation are untouched. Known trade-off: “later this year” silently expires on 2026-12-31 — unlike “Fall 2026” it will read as current while being false, and this repo has no build step, CMS, or test that could flag it, so it needs a manual calendar reminder to revisit.
All URLs are canonicalized to https://www.grandeldercare.com/ (the www host, matching CNAME).
index.html<head>carries the canonical link,theme-color, Open Graph tags, atwitter:cardtag, and JSON-LD (Organization+WebSite). Onlytwitter:cardis set for X/Twitter — scrapers fall back to theog:*tags for title/description/image, so there's one canonical copy of each string instead of hand-synced duplicates.- The deploy workflow excludes
README.mdandgoogle-apps-script/from the published site. Why: GitHub Pages was serving the whole repo, so internal strategy notes and the waitlist backend source (including the spreadsheet ID) were live URLs — and robots.txt + the sitemap would have invited crawlers to index them. - The social share image is
assets/og-image-v3.jpg, 1200×630 (the standard large-card size for iMessage, Slack, and social previews). It is a wide shot of a Grand user at home with a sensor visible in the wall outlet, with thegrand ·wordmark and the headline “You'll know she's okay” set into the empty wall space on the left. Trimmed from 16:9 to 1.91:1 by cropping height only (18px off the top, 45px off the bottom at the 1672px source width) so her face, the sensor, and the headline all clear the frame. Superseded:assets/og-image-v2.jpg(no longer referenced) andassets/og-image.jpg(still referenced by the Grace subpage). - Known limitation: the baked-in text does not survive a centred square crop. Because
og:image:width/heightare declared at 1200×630, the surfaces that matter for a shared link render the full 1.91:1 card — iMessage, X (summary_large_image), Facebook, LinkedIn, Discord, and Slack (Slack only falls back to its small square thumbnail when an image is small or its dimensions are undeclared). WhatsApp's compact preview is the real exception, and it is inconsistent about when it applies one. Measured against the centred 1:1 safe band (x 285–915of the 1200px canvas): thegrand ·wordmark sits atx 121–260and survives 0%; the headline spansx 118–694and survives 51%, i.e. it cuts mid-word, which reads as a broken render rather than as a plain photo. This is structural, not a crop-tuning problem — the subject occupies the right half of the frame and the square-safe zone is the middle 630px, so the 576px-wide text block would have to shift 165px right onto her face to survive. The trade was taken deliberately: the large-card surfaces are where shared links actually land. - Safe-area rule for future share cards. Keep the wordmark and any words inside the centre 630×630 of the 1200×630 canvas (
x 285–915). Anything outside that band is large-card-only by definition. If a future card needs its text to survive every surface, the photo has to be composed for it — subject further right and smaller, leaving centre wall free — which costs sensor visibility, so decide it at art-brief time rather than at review time. robots.txtallows all crawlers and points at the sitemap.sitemap.xmllists the public pages (/,/privacy.html,/terms.html); bump a page's<lastmod>when its content changes meaningfully, and add entries as the site grows.welcome.htmlis deliberately excluded because it'snoindex.- Favicons:
favicon.svgis the source of truth (serif "g" on the charcoal--surface-charcoalrounded square);favicon.ico(32px) is the legacy fallback that crawlers request blindly, andapple-touch-icon.png(180px, square-cornered because iOS applies its own mask) covers iOS home-screen bookmarks. - Google Search Console: the site is verified via the
google-site-verificationmeta tag inindex.html(URL-prefix property forhttps://www.grandeldercare.com/). Don't remove that tag — verification lapses without it. After content changes, the sitemap doesn't need resubmitting; Google re-crawls on its own.
- Meta Pixel is installed in
index.htmlandwelcome.htmland tracks the standardPageViewevent on load. - Reddit Pixel is installed in
index.htmlandwelcome.htmlwith pixel IDa2_jb07ge9fad9nand tracks the standardPageVisitevent on load. - Conversion events fire from
script.js(not the pixel snippets): a waitlist signup firesfbq('track','Lead')+rdt('track','SignUp'), and completing the profile page firesfbq('track','CompleteRegistration')+rdt('track','Lead'). All track calls are guarded, so a blocked or absent pixel never throws.
The site sends privacy-conscious website analytics to the same PostHog Cloud US project as the Grand iOS app, with a strict namespace boundary. posthog.js loads the US ingestion endpoint in cookieless_mode: "always", limits person profiles to identified alpha candidates, disables general autocapture, exception capture, feature-flag requests, and session recording, then manually captures pageviews. Web-vitals capture is explicitly enabled and emits PostHog's $web_vitals events independently of general autocapture. Every website event carries platform = "web" and analytics_surface = "marketing_website"; custom events also use a website_ prefix so they cannot be confused with the iOS taxonomy.
The deliberate website events are website_cta_clicked, website_contact_clicked, website_waitlist_started, website_waitlist_signup, website_waitlist_submission_failed, website_profile_completed, website_profile_submission_failed, and website_onboarding_call_clicked. Event properties describe only the page/experience, CTA location/label, form type, or fixed failure reason. At a successful waitlist submission the browser generates a random UUID candidate_id, stores it only for that browser tab's session, calls PostHog identify(), and sends the same ID to the Google Sheet. This makes the candidate's PostHog journey findable from their Sheet row while keeping phone numbers, email addresses, names, ZIP codes, and free-text answers out of PostHog. Earlier anonymous PostHog events from the same session are linked to the identified candidate; the ID deliberately does not persist across a later browser session or another device.
PostHog project dependency: in the shared PostHog project's Project settings → Web analytics, enable Cookieless server hash mode before deploying this integration. Web vitals are enabled explicitly in the JavaScript SDK configuration, so they work while the general autocapture and remote feature-flag requests remain disabled.
Open index.html in a browser. No build step is required.
Production is served by GitHub Pages at:
https://www.grandeldercare.com/
Because this is a static site with no build step, the Deploy GitHub Pages workflow copies main to the gh-pages branch on every push to main. The CNAME file keeps the custom domain attached to the Pages deployment.
The waitlist form posts to a Google Apps Script web app and appends rows to a Google Sheet.
The current configured Sheet is Grand Waitlist: https://docs.google.com/spreadsheets/d/1i2_lUmRSIVA1iN3zaE8mLrR-8QEpOUPKtM8Y6aHfw1w/edit
For a fresh setup:
- Create a Google Sheet for the waitlist.
- In the sheet, open Extensions -> Apps Script.
- Paste the contents of
google-apps-script/waitlist.gs. - Optional but recommended: paste the Google Sheet ID into
SPREADSHEET_IDat the top of the Apps Script file. - Deploy as a Web app.
- Set "Execute as" to yourself and "Who has access" to anyone.
- Copy the
/execweb app URL. - Paste that URL into
index.htmlon the waitlist form'sdata-waitlist-endpointattribute.
When updating the Apps Script code, use Deploy -> Manage deployments -> Edit -> New version. Saving the code alone does not update the deployed web app. Visiting the /exec URL directly should return JSON with spreadsheet_url, waitlist_last_row, and event_last_row; this confirms which spreadsheet the script is writing to.
The client sends the tester's phone number (the Grand site is tagged product = "grandphone", so signups land in the Grand phone number alpha list tab — see the product-routing note below), the random non-personal candidate_id shared with PostHog, plus source, page URL, referrer, user agent, user-agent client hints where available, language, timezone, viewport, screen, connection hints, a coarse IP-derived geo object when the lookup has already completed (city/region/country/postal, best-effort), and other browser metadata. The initial phone capture uses sendBeacon with a keepalive fetch fallback so the visitor can advance immediately instead of waiting for the Google Apps Script round trip. The request uses a simple text/plain POST because Google Apps Script web apps are easiest to call from a static GitHub Pages site without a CORS preflight.
Privacy note: the IP geolocation lookup sends the visitor's IP to a third party (ipapi.co) and we store their coarse location. If the site gains a privacy policy, it should disclose this. The free ipapi.co tier is ~1,000 lookups/day, which is ample at current volume — revisit (or add an API key) if traffic grows.
After queueing the signup, index.html stores the phone number in sessionStorage under grand_signup_phone and redirects to welcome.html, a noindex page that collects five optional qualifying fields: full_name, email, zipcode, reason_interested, and lives_alone (yes/no/not_sure). It POSTs a { type: "waitlist_profile", product: "grandphone", phone, ... } payload to the same endpoint. phone stays the identity used to match the row; the optional email, if given, is written into the row's existing email column (only when non-empty, so an empty submission never clears it). The alpha tester question is no longer a yes/no field — instead, the confirmation screen shown after the profile form is submitted (headed "Fast track getting set up as a test user of Grand") invites the tester to book an onboarding call via a primary "Schedule a 20-minute call with us" button linking to Calendly (https://calendly.com/d/dz47-vkm-rb2/grand-early-tester-program), so no alpha_tester value is collected from the form anymore. The backend still tolerates the field for older submissions.
handleWaitlistProfile_ in waitlist.gs looks up the person's existing row by phone (digits-only match so formatting differences don't matter, most-recent match wins) and updates that row in place — one row per person, no duplicates. It matches by phone for phone-based products and by email otherwise. Because the initial signup is queued optimistically, the profile handler briefly retries the lookup before appending a standalone profile row. If the identifier is missing or still unmatched, it appends a standalone profile row so the answers aren't lost. The profile form itself submits fire-and-forget (sendBeacon with a keepalive fetch fallback), so the confirmation screen appears instantly instead of blocking on this round trip (the script lock plus the retry loop can take a second or more). This mirrors the initial signup capture; the fallback-row behavior means nothing is lost even if the beacon is dropped.
ensureHeaders_ now auto-migrates the live sheet: because new columns are only ever appended to the end of HEADERS (geo_*, then the profile fields, phone, and candidate_id), it rewrites the header row in place when the sheet has fewer columns than HEADERS, so no manual column setup is needed after deploying a new version.
The same Apps Script endpoint also receives anonymous interaction analytics. For the Grand site these route to the Grand phone number events tab (via the grandphone product tag); the legacy Events tab is the email-era archive. The site records section views, link/button clicks, and waitlist funnel events (waitlist_phone_focus, waitlist_submit_attempt, waitlist_submit_success, waitlist_submit_error, and the profile-page equivalents waitlist_profile_submit_attempt, waitlist_profile_submit_success, waitlist_profile_submit_error). These events use a per-browser-tab session_id stored in sessionStorage; they do not include the tester's phone number.
Every content section leads with a standardized eyebrow (uppercase, 12px, clay --status-clay) above its title. The hero itself has no eyebrow — the headline leads directly. The .eyebrow is excluded from the > p intro-paragraph rules so it always renders at the base 12px.
- Hero promise ("You'll know she's okay.") with a primary "See how Grand works" CTA and a secondary "Be first to know" waitlist CTA.
- "The worry" problem section: a two-column narrative with the independence/worry copy on the left and an iMessage-style multi-day concern graphic on the right, followed by a compact strikethrough list dismissing pendants/watches, call-for-help buttons, in-home carers, and cameras with one-line stories.
- "How Grand works" section titled as such, with a combined lead ("There's a better way to know they're okay. A small hub and a few sensors. No cameras, nothing to wear, nothing to charge.") and two product cards, each showing a real product photo:
assets/grand-sensor.jpg(sensor in a wall outlet) andassets/grand-hub.jpg(hub on a kitchen counter), both optimized to ~120–210KB JPGs. The.card-mediaslot renders a cover-fit image via:has(img), falling back to a dashed placeholder when no image is present. - "What Grand pays attention to": everyday activity, the kitchen (meals), and a call for help.
- "Caregiver experience": the daily "she's okay" app view with real iOS app screens, plus a parent-perspective dignity note.
- "The Grand call center" (
#response): what happens in an emergency — a mirrored two-column section (photo left, copy right) with a four-step numbered process (real person calls through the hub/sensors, confirms she's safe, calls EMS if not, family stays notified and can join the call). Step numerals are bare clay Georgia counters via CSScounter(); the photo slot falls back to the standard dashed placeholder if the image is removed. - Waitlist form with validation and Google Sheets handoff. On success it redirects to the post-signup profile page.
- Post-signup profile page (
welcome.html,noindex): optional full name, ZIP, reason for interest, whether the person Grand is for lives alone, and alpha-tester interest, styled with the sharedstyles.css.profile-*rules. - Site footer: a top row ("Contact us" label +
hello@grandeldercare.comon the left, nav links — including Privacy Policy and Terms of Service — as a right-aligned single column) above a bottom bar with thegrand.logo bottom-left and the copyright bottom-right.
/grace is an independent product-experiment site for the Grace companion — a voice
companion that sits on a parent's kitchen counter. It began as a de-branded duplicate
of the main Grand page and is now the only Grace page (the earlier /gracecompanion
and /gracephone experiments were removed; see the consolidation note in Status).
- Independent, no cross-links. There are deliberately no navigational links
between Grand and
/grace— the only way to reach it is to type/visit its URL. It is also left out ofsitemap.xmlso it isn't publicly discoverable via SEO. - Self-contained folder (isolation over DRY).
grace/has its ownindex.html,welcome.html, and own copies ofstyles.cssandscript.js, so editing it can never affect the Grand site. The tradeoff: style changes made to the root site must be re-applied here by hand. Shared images use root-absolute paths (/assets/...) so they resolve from the subfolder. GitHub Pages serves/gracefrom its in-folderindex.htmlautomatically; the deploy workflow needs no config. - Page structure. Hero, The worry (
#worry, centered serif + three real interview quotes, grounded byassets/worry-alone.jpgin a.worry-leadtwo-column grid that collapses to one column ≤900px), What Grace does (#what-grace-does), How Grace works (#how-grace-works), Grace helps her reach out (#reach-out), Caregiver experience (#caregiver), What Grace isn't (#what-grace-isnt), and the Early access waitlist (#waitlist). CTA/profiling copy is tester-focused ("Become a tester", an early Grace tester note, "ZIP code of your loved one"). - "What Grace does" is list-formatted. All three blocks use the
.does-listbold-lead-in style (see the Status note) so the section scans consistently. - Post-signup flow. After a waitlist signup
/graceredirects to its in-folderwelcome.htmlprofiling page (same five optional fields as Grand, rebranded to Grace). - Ad pixels. The Meta/Reddit pixels reuse Grand's pixel IDs; Grace conversions can
be split from Grand's by filtering on URL in Ads Manager. The footer links to the
shared root
/privacy.htmland/terms.html(no Grace-specific legal pages). - Known a11y note: the terracotta eyebrow labels (
#b85f4a) sit at ~3.9:1 on cream and ~3.4:1 on the stone band, below WCAG AA 4.5:1; left unchanged pending a brand-color decision (body text and dark pill buttons pass).
/grace POSTs to the same Apps Script endpoint as the Grand site. Its signups
carry product = "gracecompanion" (set in grace/script.js, and also on the profile
and analytics payloads plus product-scoped sessionStorage keys), so waitlist.gs
routes them to the dedicated GraceCompanion Waitlist/GraceCompanion Events tabs in
the shared spreadsheet — kept as-is after the consolidation for data continuity.
PRODUCT_SHEETS+sheetNamesForProduct_()map each product to its own tabs:gracecompanion→ the GraceCompanion tabs, andgrandphone→ theGrand phone number alpha list/Grand phone number eventstabs (the Grand site's current tag). Payloads with no/unknownproductstill fall through to the legacyWaitlist/Eventstabs, which now hold only the historical email-era data. (Thegracephonemapping remains inwaitlist.gsbut is unused.)- New tabs are auto-created by
getSheet_+ensureHeaders_on first write.
Deploy step (required for routing to take effect): after changing waitlist.gs,
re-deploy the existing web app via Deploy → Manage deployments → Edit → New
version so the endpoint URL stays identical.
privacy.html and terms.html are standalone pages sharing the site header, footer, and stylesheet. They use the .legal-* rules in styles.css: a centered 760px reading column, an .legal-header block (eyebrow, title, "Last updated" line, intro paragraph), and .legal-body prose with serif Georgia section headings, --ink-soft body text, clay underlined links, and a .legal-contact call-out card at the end.
- Privacy Policy covers what's collected (waitlist email; optional full-name/ZIP/reason/lives-alone/alpha-tester answers; automatic technical data; cookies/pixels), how it's used and shared, retention, security, children's privacy, and GDPR/CCPA-style choices. Its "Advertising and conversion tracking" section states plainly that joining the waitlist is treated as a conversion event and shared with advertising partners (Meta, Reddit) to measure campaigns — the fact the team asked to disclose.
- Terms of Service is pre-launch boilerplate: it makes clear the product/service is not yet available and the waitlist is not a purchase or a guarantee, plus eligibility, acceptable use, IP, an explicit "not an emergency/medical service" clause, disclaimers, limitation of liability, and Delaware governing law.
- Unlike
index.html/welcome.html, the legal pages do not include the Meta/Reddit pixel snippets. They do include cookieless PostHog pageview analytics so overall site navigation is complete. They are indexable (canonical tags set, listed insitemap.xml). - Both use the correctly-spelled contact address
hello@grandeldercare.com. Confirm the governing-law jurisdiction (Delaware placeholder) and have counsel review before relying on these.