Skip to content

Release: launch-ready marketing site (33 PRs since last main update)#37

Merged
MP2EZ merged 34 commits into
mainfrom
preview
May 26, 2026
Merged

Release: launch-ready marketing site (33 PRs since last main update)#37
MP2EZ merged 34 commits into
mainfrom
preview

Conversation

@MP2EZ
Copy link
Copy Markdown
Owner

@MP2EZ MP2EZ commented May 26, 2026

Production cutover: splash → full marketing site

This is the launch PR. `main` has been at the "Coming Soon" splash since December 2025; `preview` has accumulated 33 PRs of compliance/audit/launch-readiness work. Merging this exposes the full marketing site at `being.fyi` so the App Store / TestFlight review can proceed.

Pre-merge gate (already verified)

  • ✅ Local prod build with `NEXT_PUBLIC_SHOW_FULL_SITE='true' npm run build && npm run start`: `/` returns 307 → `/home`; `/download` H1 is "Coming soon to iOS and Android" (not "Download Being"); /home, /privacy, /crisis, /support all return 200
  • ✅ User confirmed `being.fyi` custom-domain → Cloudflare prod Worker mapping is wired
  • ✅ All 33 PRs already shipped to `preview` deployment and have been live + observable on `being-website-preview.palouselabs.workers.dev`
  • ✅ CI on this PR (lint + tests + wrangler-smoke) runs on `pull_request: branches: [main, preview]`

What ships (grouped)

Launch-readiness — this session (PRs #31#36)

Compliance + legal sweep — May 2026 (PRs #24#29)

A11Y + design system (PRs #1, #2, #6, #7, #16, #17, #23, design-system 1.8 bump)

Test infrastructure (PRs #3, #5, #8, #9, #11, #12, #14, #18, #19, #20, #21, #22)

Net diff: 75 files changed, +11566 / -7983 (largely `package-lock.json` churn from DS 1.8 + Next 16.0.10)

⚠️ Post-merge manual smoke (5-min checklist)

CI is good for code correctness but doesn't smoke-test the production domain after deploy. After this PR merges and `.github/workflows/deploy.yml` runs:

```bash

Step 1: confirm being.fyi serves the new site

curl -sI https://being.fyi/ # expect 307 → /home (or 200 if Cloudflare follows redirects)
curl -sI https://being.fyi/home # expect 200
curl -s https://being.fyi/download | grep -q 'Coming soon' # expect match (NOT 'Download Being')
curl -sI https://being.fyi/privacy # expect 200 + Strict-Transport-Security header
```

Then:

  • Step 4 (browser): load https://being.fyi/ in Chrome, click through nav (Home, Philosophy, Features, Get Early Access, 988 Crisis Support), verify each lands on the right content. Check the footer renders with dark midnight background.

Out of scope (separate concerns)

  • App Store Connect metadata fields (privacy URL, support URL, marketing URL pointing at `https://being.fyi/*\`)
  • App Store screenshots, age rating, description
  • The 11 follow-ups in `docs/audits/2026-05-25-visual-audit.md` (UX-judgment items like unifying 988 callout treatment, adding /features bottom CTA, etc.)
  • CI hardening to prevent the prod-cutover blind spots noted above (separate planning step)

What "ready to submit to TestFlight" looks like after this merge

  • ✅ Privacy URL: `https://being.fyi/privacy\` (with subpages `/privacy/california` and `/privacy/multi-state`)
  • ✅ Support URL: `https://being.fyi/support\`
  • ✅ Marketing URL: `https://being.fyi/home\` (or `/`, which redirects)
  • ✅ No "available now" claims on the website
  • ✅ No broken download links
  • ✅ Crisis resources page accessible without app-install
  • ✅ Cookie disclosure is accurate
  • ✅ Global Privacy Control honored

🤖 Generated with Claude Code

MP2EZ and others added 30 commits May 22, 2026 18:26
Replace raw Tailwind utilities (bg-yellow-50, bg-red-50, border-red-300,
text-red-*) on /hipaa and /crisis 911-emergency callouts with design-system
status tokens. Extends the existing @theme inline aliases in globals.css with
warning/error/critical background twins and a critical foreground alias.

Resolves audit UX-08 (.audit-report.md). The /crisis 988 section already used
bg-crisis-bg (calm blue, intentional) and is unchanged.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a visually-hidden skip link as the first focusable element on the main
layout fragment, revealed on focus. Annotates <main> with id="main" and
tabIndex={-1} so focus lands inside content after activation.

Resolves audit A11Y-09 (.audit-report.md) — the /accessibility page advertises
this feature on line 90 but it was never wired up. Restores WCAG 2.1 AA
conformance (2.4.1 Bypass Blocks).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds vitest as a devDependency with test/test:watch scripts and a
vitest.config.ts that aliases @/ to repo root and runs in the node
environment (Cloudflare Workers' nodejs_compat flag matches).

Covers the waitlist POST handler across all 8 branches:
- missing/invalid email (400)
- missing NOTION_TOKEN or NOTION_WAITLIST_DB_ID (500)
- no A/B variant (Notion payload without Variant, no trackConversion)
- variant 'A' assigned (Variant select + trackConversion called)
- Notion API non-ok response (500)
- malformed request body (500 via outer catch)

Resolves audit TEST-08 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ic (#5)

Covers lib/ab-testing.ts:
- assignVariant: default 50/50, weighted (0.9/0.1), edge weights [1,0]/[0,1],
  fallback when weights sum < 1, distribution sanity check over 2000 draws.
- getVariant: cookie A/B/absent/invalid via mocked next/headers cookies().
- getVariantFromRequest: equivalent cases via NextRequest cookie header.
- trackConversion: logs in development, silent in production.

Branched off chore/test-vitest-waitlist so the Vitest scaffold is available
locally; rebase onto preview after PR 3 lands.

Resolves audit TEST-09 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
middleware.ts: A/B cookie's `secure` flag was gated on
`process.env.NODE_ENV === 'production'`, which is not reliably 'production'
on Cloudflare Workers. Set unconditionally to `true` — the cookie is non-
essential and there's no scenario where we want it shipped over HTTP.

next.config.ts: add `headers()` returning HSTS, X-Frame-Options DENY,
X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and CSP for
all routes. CSP allows the Notion API (waitlist) and Cloudflare Turnstile
(used in the follow-up SEC-08 PR).

Resolves audit SEC-09 + SEC-11 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsconfig.json: add noUncheckedIndexedAccess, exactOptionalPropertyTypes,
noImplicitOverride, noFallthroughCasesInSwitch, noImplicitReturns.

lib/ab-testing.ts: refactor ExperimentConfig from {variants[], weights[]}
to entries: {variant, weight}[] so the two halves can no longer drift in
length. Add non-null assertions to crypto.getRandomValues result and the
fallback-variant access (both are provably safe at runtime).

Test fixes surfaced by the new strict mode:
- waitlist route.test.ts: fetchSpy.mock.calls[0] is T|undefined under
  noUncheckedIndexedAccess; add ! assertion where the prior expectation
  already proved the call happened.
- ab-testing.test.ts: stubRandom's mockImplementation signature was a
  too-loose ArrayBufferView; cast through typeof crypto.getRandomValues
  to satisfy the typed-array overloads.

Resolves audit TS-12 + TS-13 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/philosophy was a 554-line 'use client' page hydrating an entire React
component tree just to support 4 accordion buttons. Most of the page is
static prose that benefits from SSR.

Extract the 'Deepen Your Practice' accordion section into a new
components/philosophy/PracticeAccordions.tsx client subcomponent that
owns the useState + toggle logic. Convert app/(main)/philosophy/page.tsx
to a server component (removed 'use client', useState import,
toggleSection handler) and reference <PracticeAccordions /> in place.

Initial HTML now carries all the static prose (hero, principles,
comparisons, CTA) — only the accordion section hydrates. One-open-at-a-time
behavior preserved.

While extracting, also wrap the decorative emoji and +/- indicators in
aria-hidden spans and add aria-expanded / aria-controls on each accordion
button so the section is properly announced to screen readers.

Resolves audit PERF-W-01 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- MobileBottomTabs: expand 'Phil'/'Feat' to full words for screen readers
  (icons preserve the visual abbreviation); wrap emoji in aria-hidden;
  add aria-current='page' on the active tab; aria-label on the nav.
- MobileHeader: aria-label='988 Crisis Support' on the compact 988 pill.
- home page: wrap the three '→' CTA arrows in aria-hidden spans so they
  aren't announced as 'right arrow'.
- (standalone) splash: replace the placeholder-as-sole-label anti-pattern
  with a visually-hidden <label htmlFor>; drop the redundant aria-label.

Resolves audit A11Y-10, A11Y-11, A11Y-12, A11Y-14 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The splash page (a 'use client' module) was calling redirect() from
next/navigation at top level — a server-context function invoked from
a client module doesn't redirect cleanly and bloats the client bundle
with a dead-code-shaped branch.

Move the NEXT_PUBLIC_SHOW_FULL_SITE check into (standalone)/layout.tsx,
which is a server component by default. The redirect now runs before
the client splash bundle ships; the page itself stays client-only for
the form state.

NEXT_PUBLIC_* is still build-time-inlined; replacing with a runtime
env var would be a follow-up. This PR only fixes the redirect-context
bug flagged in PERF-W-02 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace `email.includes('@')` with a zod schema (`z.string().email()`).
Catches malformed bodies, restricts to the email shape we expect, and
gives the API a clean validation boundary at the entrypoint.

Also drops the unused `Variant` type import (pre-existing lint warning).

Tests unchanged — the schema rejects the same cases the manual check did
(missing email, invalid email) with the same 400 'Valid email required'
response.

Resolves audit TS-14. Companion Turnstile / rate-limit work (SEC-08)
deferred until the waitlist starts seeing real traffic — current volume
doesn't justify the operational overhead.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six versions of catch-up. Notable changes the website can now adopt:

- 1.5.0 — therapeutic.* color palette (breathing/breathingLight/
  breathingBackground) for flow-agnostic components
- 1.6.0 — published artifact alignment (was previously installing 1.5.0
  against a 1.0.1 caret range)
- 1.7.0 — typography CSS variables (--text-{style}-{size,weight,
  tracking,leading}) under /css output, plus font-weight scale
  (--font-weight-{light,regular,medium,semibold,bold,black}). Enables
  removing hardcoded typography values from globals.css .legal-content
- 1.7.0 — web.ts adapters rebuilt as literal types: spacing[4] now
  types as '0.25rem', not string
- 1.8.0 — new /a11y subpath: getContrastRatio, meetsWCAGAA,
  touchTargets (WCAG 2.5.5/2.5.8)
- 1.8.0 — new `pairings` token (12 curated FG/BG pairs with WCAG
  levels) re-exported from package root

No code changes required by this bump; new exports are additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore(deps): bump @mp2ez/being-design-system from ^1.0.1 to ^1.8.0
Adds 15 render smoke tests covering every server-component page
(`tests/pages.smoke.test.tsx`). Each test imports the page module and
asserts `renderToString` produces non-trivial markup. Catches build-time
failures (broken imports, missing exports, runtime exceptions in JSX)
that `next build` is the only safety net for today.

The (standalone) splash is excluded — its 'use client' + stateful hooks
don't survive renderToString cleanly. Worth a separate
@testing-library/react render in a follow-up.

vitest.config.ts changes:
- Adds @vitejs/plugin-react for JSX/TSX transforms.
- Adds a tiny inline `md-raw-loader` plugin so .md imports work in Vitest
  (next.config.ts's raw-loader rule is Turbopack-only; Vitest uses Vite).
- Keeps `environment: 'node'` — renderToString is server-side and
  happy-dom's polyfilled Request/Headers break NextRequest cookie parsing
  in lib/ab-testing.test.ts. Tests needing a real DOM can opt in via
  per-file directive `// @vitest-environment happy-dom`.

New .github/workflows/ci.yml runs on every PR against main/preview:
fetches the legal content from mp2ez/being (mirroring deploy.yml's step),
installs deps, runs lint, runs tests. Previously zero PR-time gating
existed — only push-to-main/preview triggered the deploy workflow.

Resolves audit TEST-11 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns (#16)

DS 1.8 finally ships typography tokens (--text-headline1-size,
--text-title-size, --text-bodySmall-size, etc.), unblocking the
.legal-content migration that the audit flagged as blocked.

Refactor all hardcoded values in the .legal-content block to reference
DS tokens:
- Font sizes / weights / tracking → --text-* tokens
- Margins / padding → --spacing-* tokens (4px-stepped)
- Border radii → --radius-medium

Two intentional overrides documented inline:
- h2 keeps font-weight 700 (DS headline4 is 600) — legal H2s need
  stronger separation between sections.
- p keeps line-height 1.75 (DS bodyRegular is 1.5) — legal pages
  benefit from extra airiness for dense reading.

The h1 size shrinks 2px (2.25rem → 2.125rem via --text-headline1-size)
to align with the DS scale; visual impact is negligible.

Resolves audit UX-09 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep across app/ and components/. The @theme inline aliases in
globals.css already map rounded-sm/md/lg/2xl → rounded-small/medium/
large/xxl, so this PR is a pure rename — zero visual diff.

Substitutions (with \b boundaries, ordered to avoid 2xl/xl collision):
- rounded-2xl → rounded-xxl  (24px)
- rounded-xl  → rounded-large (12px; Tailwind's xl default = DS large,
                so this collapses two semantic names into one)
- rounded-lg  → rounded-large (12px)
- rounded-md  → rounded-medium (8px)
- rounded-sm  → rounded-small (4px)

76 occurrences migrated across 14 files. rounded-full and rounded-none
are left as-is (no DS equivalent).

Aliases in globals.css are intentionally NOT removed — third-party / DS
package CSS may still emit those class names, and removing the aliases
would silently shift to Tailwind v4 defaults (e.g., rounded-md would
drop from 8px to 6px). The aliases stay as a safety net; this PR is
purely about JSX semantic correctness so CLAUDE.md's rule ("Radius:
small/medium/large NOT sm/md/lg") is followed.

Resolves audit UX-10 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `wrangler-smoke` job that runs in parallel with the existing
`test` job. Builds the worker with OpenNext, boots `wrangler dev` in
the background, polls /. once it responds posts `{}` to /api/waitlist
and asserts 400 'Valid email required'.

This exercises the route handler in the real Workers runtime —
catching CF-specific divergences in `next/headers` cookies(), the
marked() import, or any future runtime breakage that unit tests
running in node wouldn't see.

NOTION_TOKEN / NOTION_WAITLIST_DB_ID are dummy values; the smoke
test exits at the zod 400 before any Notion call, so no real-write
risk and no CI secrets needed for this job.

Stacked on #PR-A (chore/ci-render-smoke-tests) — after that lands,
this PR's diff is just the wrangler-smoke job addition.

Resolves audit TEST-10 (.audit-report.md).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three CI improvements following the test audit:

1. New `npm run typecheck` script (`tsc --noEmit`) gates the 5 strict TS
   flags that PR #9 enabled. Without this, the flags can erode silently —
   ESLint doesn't enforce TS compiler errors. Step added to the `test`
   job between lint and tests.

2. tests/pages.smoke.test.tsx now asserts specific content tokens on the
   regulated/safety-critical pages. Previously the `length > 100` +
   `contains '<'` checks were tautological — a regression that swapped
   the crisis page body for placeholder text would still pass. Now /crisis
   must contain '988' + 'Crisis Lifeline', /hipaa must contain
   'NOT a HIPAA Covered Entity', /privacy/california must contain 'CCPA',
   etc. Non-regulated pages keep the length-only check.

3. The wrangler-smoke CI job now GETs /crisis after the waitlist 400
   check and asserts: HTTP 200, '988' in body, and the three highest-
   value security headers (HSTS, CSP, X-Frame-Options: DENY). This
   validates that page rendering survives OpenNext bundling and that
   next.config.ts headers() actually ships them — neither of which the
   unit tests can verify.

Resolves audit findings TEST-12, TEST-15, TEST-19, TEST-22.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the JSON parse lived inside the outer try/catch, so a
malformed body would fall through to the generic 500 'Failed to join
waitlist' error — conflating "garbage from client" with "Notion is
down" and degrading production debuggability.

Move `await request.json()` into its own try/catch returning 400
'Invalid JSON'. The 500 codepath now means actual upstream issues
only.

Test updated: the malformed-body case now asserts 400 + 'Invalid JSON'
instead of 500 + 'Failed to join waitlist'.

Tiny contract change — the splash form constructs valid JSON and no
known external client depends on the 500 here.

Resolves audit TEST-16.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
middleware.ts had zero direct coverage. The underlying assignVariant /
getVariantFromRequest functions in lib/ab-testing.ts are well-tested,
but the composition — "if no cookie, call assignVariant, then set
cookies with secure/sameSite/httpOnly/maxAge/path" — was not.

Three new tests:
- Sets Set-Cookie with full attribute set (secure: true, sameSite: lax,
  httpOnly: false, maxAge: 2592000, path: /) when no existing variant.
- Assigns the variant returned by assignVariant (B vs A).
- Does NOT emit Set-Cookie when an existing variant is present (also
  asserts assignVariant is not called — early-return preserved).

Underlying ab-testing functions are mocked; this isolates the
middleware composition without retesting their internals.

The cookie attributes are precisely what changed in the recent SEC-11
fix (secure: true unconditionally). Without this test, that fix could
silently revert.

Resolves audit TEST-14.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Button anchor branch previously ignored the `disabled` prop silently
— anchors don't have a native disabled attribute, so passing it did
nothing (audit A11Y-15).

Refactor ButtonProps into a discriminated union:
- AsButton:  CommonProps + ButtonHTMLAttributes + `href?: never`
- AsAnchor:  CommonProps + AnchorHTMLAttributes + `href: string` + `disabled?: never`

TypeScript now rejects `<Button href="/x" disabled>Bad</Button>` at the
call site. Existing usages (~30 across the codebase) all use exactly one
of href OR disabled, so no consumer breakage.

Runtime safety net: if `disabled` ever leaks onto the anchor branch via
untyped JS (cast, spread from Record<string, unknown>, etc.), the anchor
renders as inert: `aria-disabled="true"`, `tabIndex={-1}`, `onClick`
preventDefaults. Screen readers announce it as disabled, keyboard skips
it, clicks are blocked.

New tests/button.test.tsx covers:
- anchor render when href provided
- button render when href absent
- disabled forwarding on the button branch
- inert anchor render when disabled is cast-bypassed onto the anchor
  branch (uses createEvent + fireEvent to inspect defaultPrevented
  reliably across React/native event phases)

Resolves audit A11Y-15 and TEST-17.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new test files, all using the per-file directive
\`// @vitest-environment happy-dom\` so the global vitest env stays
'node' (preserves NextRequest cookie parsing for ab-testing tests).

- tests/standalone.test.tsx (TEST-13): renders the Coming Soon splash
  via @testing-library/react, fills the email input, mocks
  global.fetch, asserts the success branch renders + the right POST
  body. Second test covers the error branch when fetch returns non-OK.
- tests/accordions.test.tsx (TEST-18): clicks the four
  PracticeAccordions buttons in sequence and asserts aria-expanded
  transitions correctly. Validates the "one open at a time" reducer
  that's the entire reason the file is a client component.
- tests/layout.test.tsx (TEST-20): renders the home page through
  MainLayout (mocking next/navigation's usePathname) and asserts the
  skip-to-content link, the mobile nav landmark, and page content all
  surface. This is the first test to exercise DesktopNav, MobileHeader,
  MobileBottomTabs, and Footer composition.

Resolves audit TEST-13, TEST-18, TEST-20, and (by consuming the deps)
TEST-21 — happy-dom and @testing-library/react are no longer unused.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ims (#27)

Three FTC §5 cleanups on consumer-facing legal pages:

1. Replace [DATE - TO BE DETERMINED] placeholders on /cookies and
   /accessibility with the actual last-updated date (2026-05-23).
   A live placeholder is worse than a wrong date — it signals the
   document is incomplete.

2. Strip the /accessibility "Testing & Compliance" section which
   asserted specific ongoing operational practices not verifiable
   in the repo: automated aXe + Lighthouse testing, manual
   VoiceOver/TalkBack/NVDA testing, and "user testing with people
   who rely on assistive technologies." If these are not active
   programs, the claims are deceptive under FTC §5.

   Replace with hedged statement: "We design to WCAG 2.1 Level AA
   standards. Report accessibility issues to accessibility@being.fyi."

3. Remove "Section 508" from /accessibility "Standards We Follow."
   Section 508 applies to federal agencies and their contractors —
   not consumer wellness apps. Claiming compliance creates phantom
   regulatory obligation.

WCAG 2.1 AA, iOS Accessibility, and Android Accessibility entries
are preserved — those are accurate voluntary design-standard
references.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gnment (#28)

Apple StoreKit introductory offers only support these durations:
3 days, 1 week, 2 weeks, 1 month, 2 months, 3 months, 6 months,
1 year. "28 days" is not a configurable duration.

The underlying App Store offer will be configured as P1M (1 month).
Marketing copy must match — a mismatch between website copy and
App Store offer is both an App Store Review Guideline 2.3.7
violation (accurate metadata) and FTC §5 exposure (misleading
offer claim).

6 occurrences updated across home, features, download, and the
coming-soon splash:
- 4× "28-day free trial" → "1 month free trial"
- 1× "28 days" → "1 month" (download page big-display)
- 1× "28-day free trial included" → "1 month free trial included"

No copy that just says "free trial" (without a duration) needs to
change.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

* fix(copy): remove "HIPAA-level" + "clinical" terminology from marketing

Same FTC §5 + regulatory-applicability.md pattern as the /hipaa
page removal: claiming "HIPAA-level" encryption/privacy when Being
is not a HIPAA-covered entity creates deceptive-practices exposure
even when technically accurate (AES-256 is fine encryption — the
issue is invoking the HIPAA brand to imply equivalence).

Per regulatory-applicability.md terminology table:
- "HIPAA-compliant encryption" → "AES-256 encryption"
- "Clinical assessment" → "Wellness self-assessment"

Three replacements:
1. home page Privacy callout: "HIPAA-level encryption" → "AES-256
   encryption"
2. features page hero subhead: "clinical assessments...with HIPAA-
   level privacy" → "wellness self-assessments...with privacy-
   first design and AES-256 encryption"
3. features page Privacy & Security section: "HIPAA-level
   encryption" → "AES-256 encryption"

Also updates the JSDoc comment in features/page.tsx to use the
correct terminology.

These references surfaced during the legal-cleanup PR series
(see #24-#28). Same legal exposure pattern, missed by the original
handoff because they were inline marketing copy, not whole pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(copy): also fix "Clinical tools" → "Wellness self-monitoring tools"

Found while doing a final HIPAA/clinical sweep — same regulatory-
applicability.md terminology pattern as the other fixes in this PR.

The home page's Science pillar copy referred to PHQ-9 + GAD-7 as
"clinical tools." Per the terminology table:
- "Clinical assessment" / "Clinical tool" → "Wellness screening"
  or "self-monitoring tool"

Also softened "evidence-based care" → "evidence-based practice"
since "care" implies clinical/treatment relationship that Being
explicitly disclaims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /hipaa page used "HIPAA Notice" terminology (term-of-art) while
disclaiming HIPAA applicability, and made voluntary commitments
mirroring HIPAA §164.524 and 60-day breach notification. The page
also carried a publicly visible "Draft document - Attorney review
required" banner and [TO BE DETERMINED] placeholders. Per the
regulatory-applicability source-of-truth doc, Being must not use
HIPAA framing in user-facing materials.

The 60-day breach notification commitment is preserved in a new
privacy-policy section under the FTC Health Breach Notification Rule
(16 CFR Part 318) — see companion PR in the being repo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /privacy-practices route was reachable by direct URL (not in
footer) and used "Notice of Privacy Practices" as the page title —
a term of art defined in 45 CFR §164.520 (HIPAA Privacy Rule).
Using that title while disclaiming HIPAA applicability creates FTC
§5 exposure via the same legal-exposure pattern as /hipaa.

The substantive content was already redundant with the privacy
policy. Per regulatory-applicability.md: "Do not claim HIPAA
compliance in code comments, documentation, or user-facing
materials." The page title violated this regardless of disclaimer
text.

Companion PRs in mp2ez/being delete the source markdown
(docs/legal/notice-of-privacy-practices.md) and update the
regulatory-applicability.md Related Documents table.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /do-not-sell route was reachable by direct URL (not in footer)
and duplicated content that now lives in the privacy policy.

CCPA §1798.135(b)(1) allows businesses that do not sell or share
personal information to publish a statement to that effect in their
privacy policy in lieu of maintaining a separate opt-out link.
Being genuinely does not sell — folding the disclosure into the
privacy policy is compliant and reduces page sprawl.

The new "No Sale or Sharing of Personal Information" section in the
privacy policy (companion PRs in mp2ez/being) preserves the load-
bearing GPC honoring statement that this page contained — needed
for TDPSA and CPA universal-opt-out compliance.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /disclaimers route was deployed but not linked from anywhere in
the website (footer link was removed earlier; TOS Section 3 link to
the singular /disclaimer was repaired by deleting the link entirely,
since TOS Section 3 carries the load-bearing medical-disclaimer
language already).

The medical disclaimer remains accessible via:
- TOS Section 3 (https://being.fyi/terms)
- The in-app Legal Documents screen (mobile app bundles
  docs/legal/medical-disclaimer.md via Metro)

Removes the route file only. The content file
docs/legal/medical-disclaimer.md stays — the React Native app still
requires it via legalDocuments.ts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Publishes the website-side companion to INFRA-151. Adds the
/privacy/multi-state route (CCPA + TDPSA + CPA + CTDPA + VCDPA survey)
that the rewritten §5.3 of privacy-policy.md now promises, and surfaces
both California Privacy Rights and Multi-State Privacy Rights from the
footer Legal column to satisfy FTC §5 reasonable-accessibility for the
newly-promised disclosures.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te (#32)

Closes the website-side technical half of INFRA-151. Middleware detects
Sec-GPC: 1, sets being_gpc cookie + X-GPC-Honored response header, and
clears the cookie when the header is absent (per-request semantics, not
persisted preferences). Client-side GpcNotice component renders an
acknowledgement on /privacy, /privacy/multi-state, /privacy/california,
/cookies, and /support — matching the scope of the public commitment in
privacy-policy.md §5.3.

Discovery confirmed the site ships zero third-party trackers, so the
"suppress trackers on GPC=1" half of the original spec collapses; the
implementation is detect-and-acknowledge, not detect-and-gate.

/cookies page rewritten to honestly disclose the two first-party
functional cookies (being_ab_variant from INFRA-93, being_gpc from this
change) — the prior "we don't use cookies" claim was inaccurate even
before this change.

Vary: Sec-GPC declared in next.config.ts headers() as intent, but
Next.js's RSC layer overwrites it in both dev and prod. Filed as a
follow-up requiring OpenNext Worker middleware to append after the
framework writes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MP2EZ and others added 4 commits May 25, 2026 14:40
…02 (#33)

LegalPage was calling marked() inside the component body on every render.
OpenNext on Cloudflare Workers re-executes server components per request
even for force-static pages, so the markdown parse ran on every request
to /privacy, /terms, /support, /privacy/california, /privacy/multi-state,
and /cookies — adding ~50ms (P50) to ~500ms (P99) of Worker CPU per page
load.

Workers analytics for being-website-preview showed 6 exceededResources
(Error 1102) failures in a 2-hour window post-INFRA-151 — the
privacy-policy.md §5.3 rewrite added ~15 lines, which pushed borderline
requests over the per-isolate CPU budget. User-reported failure had Ray
ID a017a0ac992cd43b.

Fix: memoize marked() output in a module-scope Map keyed on the content
string. Content imports are module-level (identity-stable), so the cache
is bounded by the number of legal docs (currently 6). First request on
a fresh isolate parses; all subsequent requests on the same isolate hit
the cache (<1ms).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ax (#34)

Two related Tailwind v4 migration gaps that combined to break the footer
on the deployed preview.

# Problem 1: brand-* / crisis-* / accent-* utilities weren't generated

Tailwind v4 only generates utility classes for color tokens declared
inside @theme. The design system package exposes its tokens as :root
variables, which makes them usable via raw var(--color-X) but does not
trigger Tailwind utility generation. The local @theme inline block had
aliases for sage-*, status-*, and crisis-bg, but was missing:

- brand-midnight, brand-sage, brand-blue-gray, brand-off-white
- crisis-text, crisis-border
- accent-50/100/500/600/700

Result: every className like `bg-brand-midnight`, `text-brand-sage`,
`border-crisis-border` silently became a no-op. Most visibly: the
footer's `bg-brand-midnight` did nothing, so the footer rendered with
no background — and all its dark-bg-styled text colors (text-white,
text-gray-300/400) became invisible or barely visible on the page's
white background.

Also broken (now fixed by the same change): the "Download Being" CTA
button on /home (uses bg-brand-sage), the sage-green section links on
/home, the GpcNotice's sage-green border + midnight strong text, and
every text-brand-midnight heading on /crisis.

The brand-blue-gray and brand-off-white aliases bridge a casing gap:
the design system stores those as camelCase variables
(--color-brand-blueGray, --color-brand-offWhite) but the codebase uses
kebab-case utility classes.

# Problem 2: bg-opacity-* / border-opacity-* are Tailwind v3 syntax

Tailwind v4 removed these in favor of slash syntax
(bg-color/N, border-color/N) that keeps the color and alpha coupled.
The old classes silently produce no CSS in v4. Affected:

- Footer.tsx: `bg-crisis-bg bg-opacity-10` → `bg-crisis-bg/10`
- Footer.tsx: `border-crisis-border border-opacity-20` → `border-crisis-border/20`
- DesktopNav.tsx: `hover:bg-opacity-80` → `hover:bg-crisis-bg/80`
- QRCodePlaceholder.tsx: `bg-opacity-90` → `bg-brand-midnight/90`

# Verification

- CSS bundle before: 35115 bytes, zero `.bg-brand-*` rules
- CSS bundle after: 37983 bytes, includes `.bg-brand-midnight`,
  `.bg-brand-sage`, `.text-brand-midnight`, `.bg-accent-*`, etc.
- Computed styles on /home footer after build: background-color
  rgb(27, 41, 81) = #1B2951 (brand-midnight) — confirmed via Chrome
  DevTools against `npm run start`.
- Crisis disclaimer box now renders as 10% / 20% translucent overlay
  on the dark footer (was 100% pale-blue floating on white before).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(layout): move standalone routes into (main) + suppress mobile tabs on /crisis

Five pages (/accessibility, /crisis, /support, /privacy/california,
/privacy/multi-state) lived in app/<route>/ rather than app/(main)/,
so they inherited only the root layout — no DesktopNav, no Footer,
no MobileBottomTabs. A user landing on any of them from a footer link,
search result, or shared URL had no way to navigate back to the rest
of the site except the browser back button. The 2026-05-25 visual
audit (see docs/audits/) flagged this as the highest-impact finding;
PR #31's new footer links to /privacy/california and /privacy/multi-state
made the orphaning more visible.

URLs are unchanged — route groups in parens don't affect paths.
External links, redirects, sitemaps, SEO are all unaffected. The only
mechanical update needed was four imports in tests/pages.smoke.test.tsx;
this also adds /privacy/multi-state to the smoke list (was missing).

Side effect: MobileBottomTabs now renders on /crisis. Per compliance +
ux agent review, suppressed on /crisis specifically — the bottom tabs
(Home / Philosophy / Features / Crisis) compete spatially with the
in-page 988 Call/Text buttons for the same thumb-zone attention. Per
AFSP / Zero Suicide digital safety guidance, the primary crisis action
shouldn't compete with marketing-navigation affordances at the moment
a user is in distress. Other moved pages (incl. /support) keep the
tabs since they're exploratory/help-center contexts.

Also removes /crisis's in-page "About Being" section, which became
redundant once the global Footer is rendered on the page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(audit): add 2026-05-25 screen-by-screen visual audit

Output of a Chrome DevTools MCP sweep across all 12 visible routes on
the preview deployment at desktop (1440×900) and mobile (~500) — run
after the #31#34 wave to catch any remaining silent display bugs of
the same v3→v4 / token-registration class as #34, and to surface
UX-subjective improvements for design review.

Findings: 0 critical, 1 display bug (standalone routes orphaned, fixed
in the previous commit), 8 UX-judgment, 4 nits. Baseline health is
good: zero console errors, broken images, network failures, or
horizontal overflow on any page. The #34 token fix really did fix
everything.

Audit doc was reviewed by the ux agent for a second-opinion pass on
the UX-judgment items; that pass added 4 new findings (missing CTA on
/features, undersized CTA on /philosophy, /download pricing visual
hierarchy, possible borderline contrast on /home cards). Those are
filed but not addressed yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rod (#36)

Removes the last blockers to flipping the splash gate so production
(main branch) serves the full site, which is needed for TestFlight +
App Store review. The app isn't downloadable yet — but the previous
/download page (placeholder app-store badges marked '(graphic needed)')
would be embarrassing for users and confidence-breaking for App Store
reviewers.

Three coordinated changes:

1. Extract the splash page's waitlist signup form into
   components/shared/WaitlistSignupForm.tsx so both the splash and the
   new /download can use it. Splash refactored to consume it; visual
   no-change to the splash itself.

2. Rewrite /download as the pre-launch waitlist experience:
   - Hero: 'Coming soon to iOS and Android' (was 'Download Being')
   - Primary CTA: waitlist signup form
   - REMOVED: placeholder iOS/Android badges + QR codes
   - REMOVED: duplicate bottom 'Download for iOS / Android' CTA
   - REFRAMED: pricing + system requirements as 'What to expect when
     we launch'
   - FAQ retained with one new entry ('When will Being launch?')

   Every existing CTA across the site (5 of them on home/features/
   philosophy) still routes to /download — they now land on a coherent
   waitlist signup instead of broken download buttons.

3. DesktopNav 'Download' link text → 'Get Early Access'. URL unchanged.

4. NEXT_PUBLIC_SHOW_FULL_SITE flipped to unconditional 'true' in
   deploy.yml. The splash component + redirect logic remain in the
   codebase as emergency-fallback, but the splash is now unreachable
   in deployed envs — / redirects to /home on both preview and main.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MP2EZ MP2EZ merged commit 5c99f37 into main May 26, 2026
3 checks passed
MP2EZ added a commit that referenced this pull request May 26, 2026
…ge (#39)

Closes three deployment-confidence gaps that the recent prod cutover
(PR #37) made visible.

# 1. Build with NEXT_PUBLIC_SHOW_FULL_SITE='true' in wrangler-smoke

CI previously built without the env var, so the / → /home redirect
codepath (production behavior) was never exercised in CI. Now matches
the deploy.yml build mode. Readiness probe switched from / (now 307)
to /home (always 200) so wrangler-dev startup detection still works.

# 2. Extend wrangler-smoke route coverage

Old coverage: POST /api/waitlist + GET /crisis (2 routes).
New coverage adds 4 routes:
- GET /  → 307 with location: /home
- GET /home → 200 + 'Mindfulness with meaning' body marker
- GET /download → 200 + 'Coming soon' marker, NOT 'graphic needed'
  (catches if the page regresses to the pre-#36 placeholder badges)
- GET /privacy/multi-state → 200 + 'Multi-State' body marker

Each follows the existing /crisis assertion pattern.

# 3. Post-deploy smoke against live URL (deploy.yml)

Catches the failure mode CI cannot reach: DNS misconfig, custom-domain
unbound, SSL broken, deploy succeeded-but-routed-wrong. After the
wrangler-action deploy command succeeds:
- Branch-aware URL (main → being.fyi, preview → workers.dev)
- 15s sleep for Cloudflare edge propagation
- Cache-busted curl on /home (200), /download (Coming soon marker),
  /privacy (Strict-Transport-Security header)
- FAIL exits the workflow loudly so an alert fires

Note on the 'Coming soon' assertion: temporary marker tied to
pre-launch state. When the app launches and /download swaps to real
app-store badges, update this assertion or replace with a durable
marker like the H1 text.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant