feat(landing): M4 two-fields diptych + M8 final CTA + M9 footer + /join — sage & cream - #724
Conversation
…ll primitives Builds the shared First Light landing scaffold (docs/LANDING_ENTRY_WORLD_DESIGN.md) every downstream lane composes on: palette + 3-grade glass grammar, Lenis scroll root, scroll-progress/pinned-scrub/masked-reveal primitives, and nine M1–M9 moment stubs wired into a coherent draft page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHFRrZkRsAHWTwvxhySkfe
…ily (Nick 2026-07-02) Repoints --fl-pine -> #2E3A2C (sage-ink) and --fl-green -> #5C6E58 (sage-deep) so in-flight lane work recolors automatically, and adds first-class --fl-cream/--fl-cream-high/--fl-sage-mist/--fl-sage/ --fl-sage-deep/--fl-sage-ink tokens (+ rgb twins) per the SAGE & CREAM amendment in docs/LANDING_ENTRY_WORLD_DESIGN.md. Kelly #16A34A is demoted to product-only (real screenshots + M5 signal-card replica) — never on landing/auth chrome. Updates CONTRACTS.md's palette section to the v2 table, the pine->sage-ink / kelly->sage-deep read-mapping, and the daylight-mood sentence. Swept the foundation's shared files (fonts.ts, lib/, scroll/*, index.ts, page.tsx) for hardcoded #143527/#16A34A: none found, nothing to move. One hardcoded #143527 remains in moments/M3ProductCinema.tsx (the landing-cinema lane's owned stub, out of this lane's ownership) — left in place for that lane to pick up when it replaces the stub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVnwXMnsgREVfjpGSAJPvT
…o feat/entry-world-portals
|
PR title or description contains an excluded keyword. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (3)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
PR Summary by QodoFirst Light landing moments + sport-agnostic /join invite-code resolver
AI Description
Diagram
High-Level Assessment
Files changed (24)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ea08d9fd1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <Link | ||
| href="/golf/join" |
There was a problem hiding this comment.
Route the hero join CTA through the sport-agnostic form
When a baseball player clicks the first-screen “Join your team” CTA, this sends them to the golf-only /golf/join form, which then pushes every code through /golf/join/[code]; the new sport-agnostic resolver added in this commit is /join, and the rest of the new footer/final CTA already use it. Since the landing page is now explicitly shared by golf and baseball, this path misroutes baseball invite-code users before they reach the sport fork.
Useful? React with 👍 / 👎.
| if (golfTeam.data) { | ||
| return { ok: true, sport: 'golf' }; |
There was a problem hiding this comment.
Handle cross-sport invite-code collisions before choosing golf
If the same invite code exists in both a golf team and a baseball team/invitation, /join always returns golf here and a baseball invite holder is sent to /golf/join/<code>. I checked the generators and they mint codes independently from overlapping uppercase alphanumeric alphabets (src/app/golf/actions/teams.ts:58 and src/app/baseball/actions/teams.ts:579), and uniqueness is only per table, so a cross-sport collision is possible; this resolver should reject/ask for a sport or otherwise disambiguate rather than silently preferring golf.
Useful? React with 👍 / 👎.
Code Review by Qodo
Context used✅ Compliance rules (platform):
93 rules 1. Arbitrary rounded-[1.4rem] radius
|
| @@ -0,0 +1,57 @@ | |||
| 'use server'; | |||
There was a problem hiding this comment.
1. resolvejoincode in wrong directory 📘 Rule violation ⌂ Architecture
A new server action file ('use server') was added at src/app/join/actions.ts instead of under
src/app/actions/. This violates the project requirement for server action placement and makes
server action discovery/auditing inconsistent.
Agent Prompt
## Issue description
`resolveJoinCode` is implemented in `src/app/join/actions.ts`, but server actions must live under `src/app/actions/`.
## Issue Context
This file is a server action module (top-level `'use server'`). The UI imports it from `./actions`.
## Fix Focus Areas
- src/app/join/actions.ts[1-57]
- src/app/join/join-client.tsx[18-71]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <m.div | ||
| style={{ x, opacity, rotate }} | ||
| className="fl-glass-2 pointer-events-none absolute -right-4 top-1/3 hidden h-40 w-20 rounded-[1.4rem] sm:block lg:-right-10 lg:h-48 lg:w-24" | ||
| aria-hidden="true" |
There was a problem hiding this comment.
5. Arbitrary rounded-[1.4rem] radius 📜 Skill insight ⚙ Maintainability
A custom radius rounded-[1.4rem] is used on the phone glass element, which is not one of the allowed radius tokens (8/12/16/24px). This violates the radius token enforcement rule and introduces inconsistent rounding.
Agent Prompt
## Issue description
A custom border radius utility `rounded-[1.4rem]` is used instead of an approved radius token.
## Issue Context
The radius standard allows only specific tokenized radii; arbitrary values must be avoided.
## Fix Focus Areas
- src/components/marketing/first-light/moments/M3ProductCinema.tsx[173-176]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| import type { Metadata } from 'next'; | ||
| import { JoinClient } from './join-client'; | ||
| import '@/components/marketing/first-light/first-light.css'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Join your team | Helm Sports Labs', | ||
| description: 'Enter your team invite code to join your program on Helm — golf or baseball.', | ||
| }; | ||
|
|
||
| export default function JoinPage() { | ||
| return <JoinClient />; | ||
| } |
There was a problem hiding this comment.
6. /join flow undocumented 📘 Rule violation ⚙ Maintainability
A new public /join entry point and invite-code resolution behavior was added, changing user-visible join workflow. The related current-state doc memory/features/auth-onboarding-join.md was not updated to include /join as an entry point.
Agent Prompt
## Issue description
Business behavior changed by introducing `/join`, but the corresponding memory feature documentation was not updated.
## Issue Context
There is an existing join/auth feature doc (`memory/features/auth-onboarding-join.md`) listing join entry points, but it does not mention the new `/join` route or its server-side resolution step.
## Fix Focus Areas
- src/app/join/page.tsx[1-12]
- src/app/join/actions.ts[9-57]
- memory/features/auth-onboarding-join.md[13-36]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| <Link | ||
| href="/golf/join" | ||
| className="fl-glass-1 inline-flex items-center gap-2 rounded-full px-7 py-3.5 text-sm font-medium text-[var(--fl-ecru)] transition-transform duration-200 hover:-translate-y-0.5 active:translate-y-0" | ||
| > | ||
| <span className="relative z-10">Join your team</span> | ||
| </Link> |
There was a problem hiding this comment.
8. M1 join cta misroutes 🐞 Bug ≡ Correctness
M1Hero links “Join your team” to /golf/join, which sends baseball invite-code holders into a golf-only join entry instead of the new sport-agnostic /join flow used elsewhere in this PR.
Agent Prompt
### Issue description
The landing hero CTA “Join your team” routes to `/golf/join`, which is not sport-agnostic and conflicts with the new `/join` flow introduced in this PR.
### Issue Context
M8 and M9 already route join traffic to `/join`, so M1 should be consistent to avoid misrouting baseball users.
### Fix Focus Areas
- src/components/marketing/first-light/moments/M1Hero.tsx[134-146]
### Proposed fix
- Change the “Join your team” `Link` href from `/golf/join` to `/join`.
- (Optional) Audit any other M1 join links (nav pill, etc.) for the same destination.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const trimmed = code.trim(); | ||
|
|
||
| function handleChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
| setCode(e.target.value.toUpperCase()); | ||
| if (error) setError(null); | ||
| } | ||
|
|
||
| async function handleSubmit(e: React.FormEvent) { | ||
| e.preventDefault(); | ||
| setError(null); | ||
|
|
||
| if (!trimmed) { | ||
| setError('Please enter an invite code.'); | ||
| return; | ||
| } | ||
| if (trimmed.length < 4) { | ||
| setError('Invite code must be at least 4 characters.'); | ||
| return; | ||
| } | ||
|
|
||
| setStatus('checking'); | ||
| const result = await resolveJoinCode(trimmed); | ||
|
|
||
| if (!result.ok) { | ||
| setStatus('idle'); | ||
| setError(result.error); | ||
| return; | ||
| } | ||
|
|
||
| router.push(`/${result.sport}/join/${trimmed}`); |
There was a problem hiding this comment.
10. Invite code lacks allowlist 🐞 Bug ⛨ Security
The /join flow only validates length and then interpolates the user-supplied code into the destination path, so unexpected characters (e.g., / or ?) can alter routing and produce incorrect navigation or confusing 404s.
Agent Prompt
### Issue description
The join code is not constrained to an expected character set, yet it is used both for DB lookup and to construct a route path.
### Issue Context
Existing join codes are generated from a restricted uppercase alphanumeric alphabet (no ambiguous characters). The `/join` page should reject any code containing characters outside that allowlist.
### Fix Focus Areas
- src/app/join/actions.ts[31-36]
- src/app/join/join-client.tsx[38-67]
### Proposed fix
- Server-side: validate `code` against a strict regex (e.g. `^[A-Z2-9]{4,10}$` or `^[A-Z2-9]{8}$` if codes are always 8) before querying.
- Client-side: add `pattern`, `inputMode`, and/or pre-submit validation to match the server allowlist.
- When building the redirect path, use the normalized/validated code (not raw user input).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const SCREENS: CinemaScreen[] = [ | ||
| { | ||
| label: 'Command Center', | ||
| caption: 'Roster, calendar, and tasks — the whole program, one screen.', | ||
| tint: 'linear-gradient(155deg, #1c4632 0%, #143527 100%)', | ||
| }, | ||
| { | ||
| label: 'Stats Center', | ||
| caption: 'Strokes-gained and stat lines that update themselves.', | ||
| tint: 'linear-gradient(155deg, #1a3f2c 0%, #0f2a1e 100%)', | ||
| }, | ||
| { | ||
| label: 'Decision Room', | ||
| caption: 'CoachHelm surfaces the one thing worth a conversation this week.', | ||
| tint: 'linear-gradient(155deg, #173d2b 0%, #0d2318 100%)', | ||
| }, |
There was a problem hiding this comment.
11. Retired hex hardcoded 🐞 Bug ⚙ Maintainability
New First Light moments hardcode retired pine-era color values (e.g. #143527 / rgba(20,53,39,…)) despite the contract explicitly forbidding hardcoded retired hexes, increasing the risk of future palette drift.
Agent Prompt
### Issue description
Some new First Light moments include hardcoded legacy hex/rgba values that the lane contract calls out as forbidden.
### Issue Context
`CONTRACTS.md` explicitly states to never hardcode `#143527` / `#16A34A` and to use palette tokens instead.
### Fix Focus Areas
- src/components/marketing/first-light/moments/M1Hero.tsx[29-31]
- src/components/marketing/first-light/moments/M3ProductCinema.tsx[34-49]
### Proposed fix
- Replace `#143527` and `rgba(20,53,39,…)` usages with `rgba(var(--fl-sage-ink-rgb), …)` / `var(--fl-sage-ink)` / other `--fl-*` tokens.
- Keep gradients expressed in terms of `--fl-*-rgb` variables where possible so future palette changes propagate automatically.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Both field grades now cap at sage-ink 0.5 (not 0.86/0.88) so M4 doesn't read as a second deep band alongside M3; legibility instead comes from a tight local radial pocket anchored behind each glass card. Section wrapper flips to sage-mist so the seam/mobile stack gaps read light, with the eyebrow recolored to match. Cards gain a brass double-bezel frame and a baseline row (rule + description + enter arrow) replacing the separate description paragraph + "Enter" label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
… glow, specular/shadow tokens, lit cards, gradient ring, underline wipe (Amendment 2)
…o feat/entry-world-portals
|
🤖 Mission Control — PR summary What it changes: Landing M4 two-fields diptych (daylight grade + double-bezel portal cards), M8 final CTA, M9 footer, and the Risk / areas: Mostly public landing presentation, but this PR also touches Reviewers should watch: the
Posted automatically by the Helm Mission Control sweep · read-only triage, no changes made. |
…etic button-in-button CTAs, underline wipes (Amendment 2) Applies Amendment 2 — IMMACULATE (§B.8-10, §C.11+13, §D.14) to the portals+cta lane's four owned files, layered on top of the foundation lane's newly-merged utilities (.fl-cta-glow, --fl-specular/--fl-shadow-*). - M4TwoFields: saturate/contrast grade on both field photos, one directional corner light per half (golf top-left, baseball top-right), specular lip on the portal glass cards. - M8FinalCTA: primary CTA is now button-in-button (trailing arrow in its own circle, spring easing, press physics) + .fl-cta-glow + magnetic cursor-follow (desktop pointer-fine only, dead under reduced-motion); ghost CTA gets press physics + specular; mist photo moved to its own filtered layer (so text isn't desaturated) + corner light; double-bezel inner edge gains the specular lip. - M9Footer: every text link gets .fl-link-underline; sage-ink band kept as the page's one allowed-depth surface. - /join: glass panel gains the specular lip + a faint dark hairline ring (research §2.1's "double-edge" glass recipe); Continue button becomes button-in-button with press physics (no glow — budget spent on M1 + M8). - text-balance on headlines, text-pretty on leads across all four files. Gate: npx tsc --noEmit (clean) then npx eslint on the five touched files (clean), run sequentially. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…ext (Nick) .fl-card, .fl-light-pool, and .fl-aurora's brightest blob were flat cream-high fills; demoted to cream (or a cream-high-to-cream top edge on .fl-card) so nothing larger than the 1px specular lip renders near-white.
…e-art (Amendment 3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
…o feat/entry-world-portals
…r watermark, brass-on-dark (Amendment 3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
LAYOUT BRIEF
/join) so a code from either coach never 404s./golf/loginor/baseball/login; M8 coach → live calendar booking (new tab), M8 player →/join;/join→ resolves the code server-side then hands off to the real/golf/join/[code]or/baseball/join/[code]flow./join)./joinreads top-down (eyebrow → headline → one field → button).INHERITED STATE (per resume instructions)
This lane was killed twice. On entry the worktree already had a clean HEAD (
wip: inherited sage partial work) — the second builder's dirty files had already been committed before I attached. I did not need to add a fresh preservation commit; instead I audited the inherited code line-by-line against the amended specs before touching anything further:CONTRACTS.md's CTA-architecture + photo-asset + reduced-motion sections.first-light.css(--fl-sage*,--fl-cream*,--fl-clay-rgb,--fl-brass-rgb,.fl-well,.fl-glass-1/2/3) — no retired-hex (#143527/#16A34A) hardcodes anywhere in owned files (grepped clean)./join'sresolveJoinCodeserver action against the REAL, untouched join flows:golf_teams.join_code(src/app/golf/join/[code]/page.tsx),baseball_teams.join_code+baseball_team_invitations.code(src/app/baseball/join/[code]/page.tsx) — column names match exactly, admin client usage matches the existing token-route pattern (createAdminClient), zero new tables/migrations.Input/Buttonshared-component className-override chain actually wins the sage-deep focus ring over the shared kelly default (cn()→twMerge, plus.fl-well's compound-selector CSS specificity beating the sharedInput's Tailwind classes) rather than assuming it worked.useScrollProgress+ multi-stopuseTransform), theuseIsDesktopvalue-only SSR-safe pattern, and the seam-hairline opacity keyframes against the design doc's choreography map — all sound, no changes needed.Owned files (per CONTRACTS.md ownership map)
src/components/marketing/first-light/moments/M4TwoFields.tsxsrc/components/marketing/first-light/moments/M8FinalCTA.tsxsrc/components/marketing/first-light/moments/M9Footer.tsxsrc/app/join/page.tsx,src/app/join/join-client.tsx,src/app/join/actions.ts(new — sport-agnostic invite-code entry)src/components/marketing/first-light/CONTRACTS.md(ownership-map + CTA-architecture updates for this lane's files)src/components/marketing/first-light/first-light.css(added.fl-wellinset-glass-well utility — shared file, additive only)public/marketing/first-light/photos/{golf,baseball,mist}.jpg(placeholder asset swap only, per the photo-asset contract — nothero.jpg, out of this lane's scope)Design-bar checklist
/join's field is an inset glass well (.fl-well,first-light.css) with a sage-deep focus ring, never a white or flat-black rectangle./join's panel is.fl-glass-3(the auth-form grade), not a solid card.flFraunces, no italic), Space-Grotesk-family UI type elsewhere — no Inter/system-sans introduced.useScrollProgress,prefers-reduced-motionfreezes to the static final frame (M4 static portals stacked,/join's entrance skips its fade), mobile (390px) verified by inspection as calm/static (M4 stacks, no seam, no scrub)..fl-glass-*surface at a time;/joinshows one.fl-glass-3panel.<a href target=_blank>(degrades without JS), player CTA and M9's every link resolve to real routes; the legacy footer's dead "Request Demo" button and golf-hardcoded paths are gone./joinkeeps the underlying flows byte-identical — zero changes to/golf/join/[code]or/baseball/join/[code], zero new tables/migrations;resolveJoinCodeonly classifies sport via a narrow read-only admin-client lookup.Flag for Nick: M8's "See it in action" points to
https://calendar.app.google/s9DBb3bKD2teLLBT7— please confirm this is the calendar you want coach demo requests landing on before this ships to production.Gate (static only — this session is browser-free, see machine-safety note below)
npx tsc --noEmit— 0 errors.npx eslinton every changed file (src/app/join/{actions.ts,join-client.tsx,page.tsx},src/components/marketing/first-light/moments/{M4TwoFields,M8FinalCTA,M9Footer}.tsx) — 0 errors, 0 warnings.Visual proof frames deferred — this session is browser-free (machine stability); frames will be captured in a later pass.