* feat(funnel): add per-form required field list
Single source for which funnel answers are required, plus the check the
intake endpoint runs a submission against.
* refactor(web): build the funnel form schema from the shared list
Form definitions no longer carry a `required` flag, they describe
rendering only. The three pages pass REQUIRED_FIELDS_BY_FORM into
ConnectFormSection, which derives the zod schema and the required markers
from it.
* fix(civi-crm): validate required funnel fields on afform-submit
Only formName, the hear-about answer and the captcha were checked, so a
direct POST could create a record with no name, email or country. A
payload missing a required answer now gets a 400 naming the fields.
* docs(funnel): document the required-field validation
* refactor(civi-crm): remove case management pages and API
The Circle Case UI, its API routes and the CiviCRM query layer they used
are gone. Keeps the app shell out entirely: no pages remain, so the
layout, styles and UI dependencies go with them.
* refactor(civi-crm): drop CiviCRM write from the intake endpoint
Submissions now go to Notion and, for the steward form, n8n. The legacy
afformCircleContactForm name was CiviCRM-only and is no longer accepted;
the fields[] payload is ignored.
* chore(web): remove CiviCRM afform generator scripts
They fetched form definitions from the CiviCRM API, which is gone. The
generated files under lib/civicrm are hand-maintained from now on.
* refactor(web): trim funnel form definitions and move to lib/funnel-forms
Fields keep only what renders the form: formKey, label, inputType,
required, repeatable. The CiviCRM entity/join/fieldName plumbing and the
fields[] POST payload are gone, along with the unused circle contact
form.
country was marked required but exempt from validation through its
CiviCRM Address join, so it is now enforced like every other required
select.
* docs: rewrite civi-crm and funnel docs for the intake-only app
Drops the CiviCRM API guide and the case management architecture, and
updates the funnel reference to the Notion + n8n flow.
TechDetailHero and TechTextSplitSection were locked to fixed heights, so
content spilled into the next section once text rewrapped. The hero's
mobile block also positioned its children absolutely inside an h-full
container, which cannot grow with its content at all.
Move that block to normal flow with the same 21/60/122px offsets, and
convert every fixed height to a minimum across both primitives and all
call sites. Section margins are untouched, so the gaps are unchanged.
The feature image follows the copy column, so it resized on every tab
switch (639/592/481/481px at the 1440 canvas). Stack all four releases
into the same desktop grid cell instead: the cell is always as tall as
the tallest release, and only the active one is visible while the rest
hold the height open. Every tab now renders the same image height and
the module table stays flush with the bottom of the image.
A fixed min-height would not work here because the tallest release
changes with the column width - at 1280px the spread is 717/639/496/481.
Mobile still renders the active release alone.
Equalise the release panel columns on desktop: the feature image dropped
its fixed 702/521 aspect and now stretches to the grid row, so its bottom
edge lands on the last module row and the module table sits flush with the
bottom of the image. Mobile keeps the 702/521 crop since it stacks.
Cap the release copy at 92ch instead of 548px so the desktop line breaks
land exactly where Figma has them (storage. / stack / making / through).
The cap sits on the element that owns font-mono-body because ch resolves
against the element's own font, and using ch rather than px keeps the
breaks stable across the Fira Mono fallback stack. Mobile is uncapped and
wraps naturally.
- Align release image height to the left column and remove the image overlay copy
- Enlarge release tabs (keep 12px min font, wider box and padding) and hide the tab scrollbar
- Normalize below-desktop section spacing (release/overview/faqs) to 120px and hide the faq divider below desktop
Phones report a viewport taller than the area they actually paint, so
centring in the full 760px hero left a visibly bigger gap above the copy
than below it — on the reported preview roughly 110px above against 56px
below.
Cap the mobile centring box at 700px. Viewports shorter than that are
unaffected and stay exactly centred; taller ones now sit 30px higher.
The hero is a fixed 760px, which is taller than most phone viewports, so
copy centred within the section still sat low on screen — on a 690px
viewport there was 114px above the block and only 36px below it.
Below `lg`, centre the copy in min(100svh, 760px) instead, floored at
560px so a landscape phone cannot clip it. From `lg` up nothing changes:
the same absolute offsets as the homepage, which keeps rendering
byte-identical markup because the new layout is opt-in per page.
Page views, bounce rate and time on site already come from the site-wide
Umami script, and UmamiButtonTracker already fires on every link click —
but it names the event after the element's text. That breaks on a copy
edit, and for the path cards the whole card is the link, so the event name
was the heading, CTA label and body run together.
Give the hero and path-card sections an optional event-name prop and set a
stable name for all seven CTAs. Both props are optional, so the homepage
keeps rendering byte-identical markup.
The centred flex layout spaced the three hero blocks evenly, but the
homepage's offsets are not even (52px above the headline, 78px below), so
the headline landed 22px lower than on the homepage.
Go back to the shared offsets layout on /build-the-parallel. Both pages now
render the hero from the same component with the same absolute positions,
and both overlay their header, so the section starts at page y=0 on each.
The hero placed its three copy blocks at hand-tuned absolute offsets, so
the group drifted a little below the section's centre and the drift moved
with the viewport and the length the copy wrapped to.
Add a `centred` content layout that stacks the same blocks in a flex
column with an even 70px rhythm, centred in the hero, and use it on
/build-the-parallel. The homepage keeps the existing offsets layout and
renders byte-identical markup.
The header reserved its 40/42px bar on this page, so a beige strip sat
above the hero. Register /build-the-parallel with the existing hero-tone
header list so the nav floats over the hero image the way Figma draws it.
Figma leaves 224px between the path cards and the footer; the shared
choose-your-path section only contributes 56px, so the page adds the
rest and scales it down below `lg`.
Move the hero and choose-your-path card sections out of sections/home so
pages other than the homepage can compose them, extract the two-line
statement heading, and lift CenterCtaSection out of the Movement page's
private atoms module.
The hero now takes an optional `background` node, defaulting to the same
Logos background video the homepage has always rendered.
Rendered output is unchanged: the homepage's hero, social proof, and
choose-your-path markup and the Movement page body are byte-identical to
develop, and the generated stylesheet loses no rules.