Skip to content

feat(landing): M4 two-fields diptych + M8 final CTA + M9 footer + /join — sage & cream - #724

Merged
njrini99-code merged 13 commits into
batch/baseball-fixesfrom
feat/entry-world-portals
Jul 2, 2026
Merged

feat(landing): M4 two-fields diptych + M8 final CTA + M9 footer + /join — sage & cream#724
njrini99-code merged 13 commits into
batch/baseball-fixesfrom
feat/entry-world-portals

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

LAYOUT BRIEF

  • Page's one job: fork the visitor into their real sport (M4), close the pitch with one clear ask per audience (M8), and never leave a dead link on the way out (M9) — plus a thin, sport-agnostic invite-code landing (/join) so a code from either coach never 404s.
  • Primary action: M4 → /golf/login or /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.
  • Visual hierarchy: full-bleed diptych photography (M4) > serif ask over misty inset (M8) > quiet two-column sage-ink footer (M9) > single glass panel, one field, one button (/join).
  • Scan pattern: M4 reads left-to-right as two equal fields meeting at a brass seam; M8 reads center-out (photo frame → headline → CTA pair); /join reads top-down (eyebrow → headline → one field → button).
  • What got demoted: kelly green off all landing/auth chrome (product-only now), clay to a single trace wash on the baseball half of M4, the old footer's golf-first bias and dead "Request Demo" button — deleted outright.

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:

  • Read both design docs' ⚠ AMENDMENT sections (sage & cream on landing; painterly baseball login dead — N/A to this lane's files) and CONTRACTS.md's CTA-architecture + photo-asset + reduced-motion sections.
  • Verified every CSS custom property referenced in M4/M8/M9/join actually exists in 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).
  • Verified /join's resolveJoinCode server 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.
  • Verified the Input/Button shared-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 shared Input's Tailwind classes) rather than assuming it worked.
  • Traced the M4 scrub math (useScrollProgress + multi-stop useTransform), the useIsDesktop value-only SSR-safe pattern, and the seam-hairline opacity keyframes against the design doc's choreography map — all sound, no changes needed.
  • Conclusion: the inherited work already fully satisfied the sage & cream amendment and the CTA architecture — nothing was off-spec, missing, or broken. Shipped as verified rather than rewritten, per "keep what serves the spec."

Owned files (per CONTRACTS.md ownership map)

  • src/components/marketing/first-light/moments/M4TwoFields.tsx
  • src/components/marketing/first-light/moments/M8FinalCTA.tsx
  • src/components/marketing/first-light/moments/M9Footer.tsx
  • src/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-well inset-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 — not hero.jpg, out of this lane's scope)

Design-bar checklist

  • Sage & cream only on landing/auth chrome — kelly appears nowhere in these files (grepped); clay is a trace wash on M4's baseball half only, exactly per the amendment's "clay's only remaining home."
  • Daylight mood — every fallback gradient grades sage-ink → sage → cream-high, never pine/black duotone.
  • No stark white input rectangles — /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.
  • No centered-white-card-on-cream — /join's panel is .fl-glass-3 (the auth-form grade), not a solid card.
  • Fraunces for display (via the scaffold's flFraunces, no italic), Space-Grotesk-family UI type elsewhere — no Inter/system-sans introduced.
  • Motion: transform/opacity only, M4's scrub built on the shared useScrollProgress, prefers-reduced-motion freezes 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).
  • ≤2 backdrop-blur layers per viewport — M4/M8 each show one .fl-glass-* surface at a time; /join shows one .fl-glass-3 panel.
  • Real handlers only — M8 coach CTA is a real <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.
  • /join keeps the underlying flows byte-identical — zero changes to /golf/join/[code] or /baseball/join/[code], zero new tables/migrations; resolveJoinCode only 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 eslint on 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.

Fable Integrator and others added 5 commits July 2, 2026 12:27
…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
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

PR title or description contains an excluded keyword.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Jul 2, 2026 8:43pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (3)
  • main
  • develop
  • release/*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8240aefd-c5fb-42ee-9a7a-0a1378af330c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/entry-world-portals
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

First Light landing moments + sport-agnostic /join invite-code resolver

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replace the home page with the First Light M1–M9 landing composition and Lenis-based scroll root.
• Add a public /join page that resolves invite codes server-side and forwards into existing sport
 join flows.
• Introduce First Light shared contracts, palette/glass CSS, and motion/scroll primitives for
 downstream lanes.
Diagram

graph TD
  V(["Visitor"]) --> L["Landing / (First Light)"] --> SL["M4: Sport login CTAs"]
  L --> CAL[["Coach calendar (external)"]]
  L --> J["/join (public)"] --> A["resolveJoinCode() (server action)"] --> DB[("Supabase")]
  A --> SJ["/golf|/baseball join flow"]

  subgraph Legend
    direction LR
    _u(["User"]) ~~~ _p["Page/Component"] ~~~ _ext[["External"]] ~~~ _db[("Database")] ~~~ _fn["Server action"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Encode sport into invite codes
  • ➕ Eliminates the need for any service-role/admin lookup on a public page
  • ➕ Allows purely client-side routing to the correct join flow
  • ➖ Requires changing code generation and potentially existing distributed codes
  • ➖ Adds long-term coupling between code format and product structure
2. Public RPC/edge function with limited privileges
  • ➕ Avoids using a general service-role client from the app runtime
  • ➕ Can enforce stricter rate limits and logging at the boundary
  • ➖ Additional infrastructure surface area to deploy and maintain
  • ➖ Still requires some privileged read to bypass current authenticated-only RLS
3. Unify invites in a shared table with anon-select RLS
  • ➕ Single query for code resolution; clearer data model for future sports
  • ➕ Removes the need to query three separate tables
  • ➖ Requires schema/migration work and potentially impacts existing join flows
  • ➖ Higher risk and longer lead time than a routing shim

Recommendation: The PR’s current approach (a narrow, read-only server action using the existing admin-client pattern to classify sport, then handing off to untouched per-sport join pages) is the best tradeoff for now: minimal schema risk and preserves existing auth/validation behavior. If abuse/rate limiting becomes a concern, consider migrating the resolver behind a dedicated RPC/edge function with explicit throttling and audit logging.

Files changed (24) +2511 / -10

Enhancement (13) +1314 / -9
actions.tsAdd server action to resolve invite code to a sport +57/-0

Add server action to resolve invite code to a sport

• Introduces a server action that normalizes the invite code and performs minimal, read-only lookups using the Supabase admin client. Returns only a sport classification (golf/baseball) or a user-friendly error message.

src/app/join/actions.ts

join-client.tsxImplement /join client UI with glass well input and redirect +178/-0

Implement /join client UI with glass well input and redirect

• Adds a single-field invite code form styled with First Light glass primitives and reduced-motion friendly entrance animation. Submits to the resolveJoinCode server action and routes to the existing sport-specific /join/[code] page.

src/app/join/join-client.tsx

page.tsxAdd /join route metadata and wire client page +12/-0

Add /join route metadata and wire client page

• Defines SEO metadata for /join and imports First Light CSS for consistent styling. Renders the JoinClient as the route body.

src/app/join/page.tsx

page.tsxReplace legacy landing with First Light M1–M9 composition +35/-9

Replace legacy landing with First Light M1–M9 composition

• Rewires the home page to render First Light moments under LenisRoot and imports first-light.css. Updates overflow handling to preserve sticky behavior required by pinned scrub sections.

src/app/page.tsx

M1Hero.tsxAdd M1 hero moment with graded photo background and CTAs +151/-0

Add M1 hero moment with graded photo background and CTAs

• Implements the landing hero with layered photo+gradient background, glass nav pill, and headline reveal. Includes reduced-motion-friendly motion behavior and top-of-page CTAs.

src/components/marketing/first-light/moments/M1Hero.tsx

M2Clarity.tsxAdd M2 clarity editorial moment with ledger lines +70/-0

Add M2 clarity editorial moment with ledger lines

• Implements the editorial statement plus three ruled ledger rows with viewport-triggered reveals. Establishes the ecru editorial register and MaskedReveal usage pattern.

src/components/marketing/first-light/moments/M2Clarity.tsx

M3ProductCinema.tsxAdd M3 pinned product cinema scrub sequence +184/-0

Add M3 pinned product cinema scrub sequence

• Creates a pinned-scrub section with a glass desktop frame cycling through placeholder screens and captions under scroll control. Includes a phone frame element that arcs in mid-scrub and a coherent reduced-motion final frame.

src/components/marketing/first-light/moments/M3ProductCinema.tsx

M4TwoFields.tsxAdd M4 diptych sport portal with scroll scrub seam flash +180/-0

Add M4 diptych sport portal with scroll scrub seam flash

• Implements the two-field diptych with scroll-linked slide-in choreography and a brass seam flash that settles to a hairline. Links directly to sport logins and falls back to a static stacked layout on mobile or reduced-motion.

src/components/marketing/first-light/moments/M4TwoFields.tsx

M5Intelligence.tsxAdd M5 intelligence moment stub with glass signal card +77/-0

Add M5 intelligence moment stub with glass signal card

• Introduces a placeholder implementation for the CoachHelm “signal” framing using a deep glass panel. Establishes structure and styling conventions for the eventual lane-owned content.

src/components/marketing/first-light/moments/M5Intelligence.tsx

M6ForThePlayer.tsxAdd M6 player-focused vignette trio stub +69/-0

Add M6 player-focused vignette trio stub

• Adds a clay-to-ink gradient section with three glass vignette cards and viewport reveals. Serves as a scaffold for the player-audience narrative moment.

src/components/marketing/first-light/moments/M6ForThePlayer.tsx

M7Honesty.tsxAdd M7 honesty band stub with animated stat +74/-0

Add M7 honesty band stub with animated stat

• Implements the honesty copy band with an AnimatedNumber-based stat and editorial text. Establishes the structure for future real proof points without introducing fake logos.

src/components/marketing/first-light/moments/M7Honesty.tsx

M8FinalCTA.tsxAdd M8 final CTA frame with coach calendar + /join link +105/-0

Add M8 final CTA frame with coach calendar + /join link

• Adds the mist photo inset within a double-bezel frame and renders the dual CTA pair with distinct real handlers. Uses reduced-motion-safe reveal timing and deep-links via id="cta".

src/components/marketing/first-light/moments/M8FinalCTA.tsx

M9Footer.tsxAdd M9 two-column sport footer with real links only +122/-0

Add M9 two-column sport footer with real links only

• Implements a sage-ink footer organized by sport with links to logins, signups, and /join plus legal/product links. Removes dead CTA patterns by only rendering resolvable routes.

src/components/marketing/first-light/moments/M9Footer.tsx

Documentation (1) +309 / -0
CONTRACTS.mdDocument First Light ownership, palette, motion, and CTA contracts +309/-0

Document First Light ownership, palette, motion, and CTA contracts

• Adds a comprehensive contract doc covering file ownership, palette tokens, glass grammar, reduced-motion patterns, scroll primitives, and CTA architecture. Serves as the coordination point for downstream landing lanes.

src/components/marketing/first-light/CONTRACTS.md

Other (10) +888 / -1
next-env.d.tsUpdate Next route type reference path +1/-1

Update Next route type reference path

• Switches the imported Next routes type definition to the dev output location. Keeps TypeScript route typing aligned with the current Next build output structure.

next-env.d.ts

first-light.cssAdd First Light palette tokens, glass grades, and inset input well +284/-0

Add First Light palette tokens, glass grades, and inset input well

• Defines sage/cream palette tokens (including legacy aliases) and the three glass surface grades with grain and fallback behavior. Adds shared utilities (rule, line masks) and an inset glass well input treatment with error-state styling keyed off aria-invalid.

src/components/marketing/first-light/first-light.css

fonts.tsAdd dedicated Fraunces font loader for First Light +25/-0

Add dedicated Fraunces font loader for First Light

• Introduces a Next.js font definition for Fraunces with multiple weights and no italics, scoped to the First Light surface. Avoids coupling to existing app-wide font exports and constraints.

src/components/marketing/first-light/fonts.ts

index.tsCreate First Light barrel exports for primitives and moments +43/-0

Create First Light barrel exports for primitives and moments

• Exports fonts, scroll primitives, shared helpers, and M1–M9 moments (plus prop types) from a single entrypoint. Establishes stable import paths and contracts for other lanes.

src/components/marketing/first-light/index.ts

photoBg.tsAdd graded background helper for photo + gradient fallback +37/-0

Add graded background helper for photo + gradient fallback

• Provides a helper to build CSS background-image layers combining a fallback gradient with an optional photo URL. Prevents broken-image UI when photo assets are missing and standardizes section photo grading behavior.

src/components/marketing/first-light/lib/photoBg.ts

LenisRoot.tsxAdd LenisRoot provider with LazyMotion and reduced-motion gating +72/-0

Add LenisRoot provider with LazyMotion and reduced-motion gating

• Introduces the single-instance Lenis scroll root and provides the LazyMotion feature bundle required by m.* components. Disables smooth scrolling on reduced-motion and coarse-pointer devices to preserve UX and battery.

src/components/marketing/first-light/scroll/LenisRoot.tsx

MaskedReveal.tsxAdd MaskedReveal component for line-by-line serif reveals +174/-0

Add MaskedReveal component for line-by-line serif reveals

• Implements a reusable masked line reveal that supports both whileInView and scrub-linked progress modes. Uses an SSR-safe reduced-motion hook for structural branching to avoid hydration mismatches.

src/components/marketing/first-light/scroll/MaskedReveal.tsx

PinnedScrub.tsxAdd sticky-based pinned scrub primitive with reduced-motion fallback +67/-0

Add sticky-based pinned scrub primitive with reduced-motion fallback

• Creates a height-vh spacer + sticky viewport primitive that passes a MotionValue progress into a render prop. Under reduced-motion, renders as a static 100dvh block with progress frozen at 1.

src/components/marketing/first-light/scroll/PinnedScrub.tsx

usePrefersReducedMotion.tsAdd SSR-safe reduced motion hook for structural branching +47/-0

Add SSR-safe reduced motion hook for structural branching

• Uses useSyncExternalStore to provide a reduced-motion value that matches between server render and first client render. Prevents hydration errors when components branch their JSX structure on reduced-motion.

src/components/marketing/first-light/scroll/usePrefersReducedMotion.ts

useScrollProgress.tsAdd scroll progress hook compatible with Lenis and cross-component transforms +138/-0

Add scroll progress hook compatible with Lenis and cross-component transforms

• Implements a manual scroll listener + bounding-rect based progress calculation with optional spring smoothing. Avoids framer-motion ScrollTimeline acceleration issues observed when passing progress across component boundaries.

src/components/marketing/first-light/scroll/useScrollProgress.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +141 to +142
<Link
href="/golf/join"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/app/join/actions.ts
Comment on lines +46 to +47
if (golfTeam.data) {
return { ok: true, sport: 'golf' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@qodo-code-review

qodo-code-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (4) 📜 Skill insights (2)

Context used
✅ Compliance rules (platform): 93 rules

Grey Divider


Action required

1. Arbitrary rounded-[1.4rem] radius 📜 Skill insight ⚙ Maintainability
Description
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.
Code

src/components/marketing/first-light/moments/M3ProductCinema.tsx[R173-176]

+    <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"
Relevance

⭐⭐⭐ High

Repo previously swept arbitrary radii to canonical tokens and added tests enforcing it (PR #149);
rounded-full rejection seems exception (PR #274).

PR-#149
PR-#274

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519419 restricts border radius values to a defined token set. The PhoneArc
element uses rounded-[1.4rem], which is an arbitrary radius outside the allowed tokens.

src/components/marketing/first-light/moments/M3ProductCinema.tsx[173-176]
Skill: modern-saas-ui

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. /join flow undocumented 📘 Rule violation ⚙ Maintainability
Description
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.
Code

src/app/join/page.tsx[R1-12]

+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 />;
+}
Relevance

⭐⭐⭐ High

Team has accepted updating memory/feature docs for new behavior/feature registration (PR 296
partially accepted).

PR-#296

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519257 requires updating memory/features/* docs when business behavior changes.
This PR adds a new /join route and a server-side resolver, while the existing join feature doc
lists join entry points but does not include /join.

Rule 1519257: Update memory feature docs when changing business behavior
src/app/join/page.tsx[1-12]
src/app/join/actions.ts[9-30]
memory/features/auth-onboarding-join.md[13-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


3. resolveJoinCode in wrong directory 📘 Rule violation ⌂ Architecture
Description
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.
Code

src/app/join/actions.ts[1]

+'use server';
Relevance

⭐⭐ Medium

No historical evidence found enforcing server actions only under src/app/actions/.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519234 requires server action implementations to live under src/app/actions/.
The added file src/app/join/actions.ts begins with 'use server', confirming it is a server
action module located outside the allowed directory.

Rule 1519234: Place Next.js server actions in src/app/actions directory
src/app/join/actions.ts[1-1]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (1)
4. M1 join CTA misroutes 🐞 Bug ≡ Correctness
Description
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.
Code

src/components/marketing/first-light/moments/M1Hero.tsx[R141-146]

+          <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>
Relevance

⭐⭐ Medium

No direct historical evidence on marketing CTA routing consistency (/join vs /golf/join).

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
M1Hero still points join traffic to the golf join entry, while the new landing CTA architecture in
this PR routes join traffic to /join (M8 + M9).

src/components/marketing/first-light/moments/M1Hero.tsx[134-146]
src/components/marketing/first-light/moments/M8FinalCTA.tsx[89-97]
src/components/marketing/first-light/moments/M9Footer.tsx[17-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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



Remediation recommended

5. Invite code lacks allowlist 🐞 Bug ⛨ Security
Description
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.
Code

src/app/join/join-client.tsx[R38-67]

+  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}`);
Relevance

⭐⭐ Medium

No clear historical pattern on allowlisting URL path segments before router.push; evidence mixed.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The client constructs router.push using trimmed without charset validation, and the server
action only checks length before performing lookups, leaving routing behavior sensitive to
unexpected characters.

src/app/join/join-client.tsx[38-67]
src/app/join/actions.ts[31-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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



Informational

6. Retired hex hardcoded 🐞 Bug ⚙ Maintainability
Description
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.
Code

src/components/marketing/first-light/moments/M3ProductCinema.tsx[R34-49]

+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%)',
+  },
Relevance

⭐⭐ Medium

No historical evidence found for enforcing “no retired hex” rule; similar tokenization feedback
often undetermined.

PR-#304

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
M1Hero and M3ProductCinema contain hardcoded pine-era values, and the local contract explicitly
instructs not to hardcode those retired hexes.

src/components/marketing/first-light/moments/M1Hero.tsx[29-31]
src/components/marketing/first-light/moments/M3ProductCinema.tsx[34-49]
src/components/marketing/first-light/CONTRACTS.md[122-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


7. Nonstandard spacing p-7 used 📜 Skill insight ⚙ Maintainability
Description
The /join UI uses spacing classes like p-7, sm:p-9, and space-y-5 which map to spacing
values outside the allowed 4/8/12/16/24/32/48/64px scale. This violates the spacing token
standardization rule.
Code

src/app/join/join-client.tsx[R106-107]

+          <div className="fl-glass-3 rounded-3xl p-7 sm:p-9">
+            <form onSubmit={handleSubmit} className="relative z-10 space-y-5" noValidate>
Relevance

⭐ Low

Spacing/token enforcement feedback commonly rejected; team hasn’t enforced strict spacing scale
historically (PR 274).

PR-#274

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519417 requires spacing to match a restricted scale. The new /join panel and
form use p-7, sm:p-9, and space-y-5, which correspond to 28px/36px/20px and are not in the
allowed set.

src/app/join/join-client.tsx[106-107]
Skill: modern-saas-ui

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Non-conforming spacing utilities are used (e.g., `p-7`, `sm:p-9`, `space-y-5`).

## Issue Context
The spacing standard allows only 4/8/12/16/24/32/48/64px equivalents; Tailwind utilities like `p-7` (28px) and `space-y-5` (20px) fall outside that scale.

## Fix Focus Areas
- src/app/join/join-client.tsx[106-107]
- src/app/join/join-client.tsx[158-165]
- src/components/marketing/first-light/moments/M4TwoFields.tsx[113-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Join resolver drops errors 🐞 Bug ☼ Reliability
Description
resolveJoinCode() uses the service-role admin client and fires three Supabase queries but never
checks .error or catches exceptions, so transient failures can be misreported as “code not found”
and unexpected exceptions can turn into 500s.
Code

src/app/join/actions.ts[R38-44]

+  const supabase = createAdminClient();
+
+  const [golfTeam, baseballTeam, baseballInvite] = await Promise.all([
+    supabase.from('golf_teams').select('id').eq('join_code', code).maybeSingle(),
+    supabase.from('baseball_teams').select('id').eq('join_code', code).maybeSingle(),
+    supabase.from('baseball_team_invitations').select('id').eq('code', code).maybeSingle(),
+  ]);
Relevance

⭐ Low

Repo often rejects adding extra try/catch or error handling around Supabase calls (e.g., PR 564,
209).

PR-#564
PR-#209

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The action does not inspect query error fields and is not guarded by try/catch, while the admin
client is implemented to throw when required env vars are missing; a nearby admin-backed lookup
already uses a defensive try/catch pattern.

src/app/join/actions.ts[31-57]
src/lib/supabase/admin.ts[4-13]
src/app/golf/actions/access-code.ts[33-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The `/join` server action treats any Supabase failure like an invalid code (or throws), because it doesn’t check query errors and doesn’t wrap admin-client creation/queries in a `try/catch`.

### Issue Context
`createAdminClient()` can throw if env is misconfigured, and Supabase queries can return `{ error }` for transient failures; both should return a user-safe error (and ideally log).

### Fix Focus Areas
- src/app/join/actions.ts[31-57]

### Proposed fix
- Wrap the body of `resolveJoinCode` in `try/catch`.
- After each query, check `response.error` and return `{ ok:false, error:'Something went wrong…' }` (optionally log server-side).
- Consider adding `.limit(1)` to match existing patterns.
- Mirror the defensive approach used in `validateAccessCode()` (admin lookup) where failures return `false` rather than throwing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
9. fl-glass-3 missing required utilities 📘 Rule violation ⚙ Maintainability
Description
The /join panel uses fl-glass-3 for a glass-effect container but does not include the required
standardized utility classes (bg-white/70, backdrop-blur-xl, border-white/20, rounded-2xl).
This violates the glass-effect standardization rule and can cause inconsistent visuals.
Code

src/app/join/join-client.tsx[R106-107]

+          <div className="fl-glass-3 rounded-3xl p-7 sm:p-9">
+            <form onSubmit={handleSubmit} className="relative z-10 space-y-5" noValidate>
Relevance

⭐ Low

Design-token/class-standardization suggestions are frequently rejected in reviews (PR 274).

PR-#274

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519253 mandates a fixed set of Tailwind utilities for any glass-effect element.
The /join glass container is marked with fl-glass-3 but its class list does not contain the
required bg-white/70, backdrop-blur-xl, border-white/20, and rounded-2xl utilities.

Rule 1519253: Enforce standardized glass-effect utility classes
src/app/join/join-client.tsx[106-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A glass-effect element does not include the standardized glass Tailwind utility set.

## Issue Context
The `fl-glass-3` container is clearly intended as a glass surface, but the required baseline utilities are missing.

## Fix Focus Areas
- src/app/join/join-client.tsx[106-107]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Dev-only routes types import 🐞 Bug ☼ Reliability
Description
next-env.d.ts now imports ./.next/dev/types/routes.d.ts, but .next/ is gitignored and the
repo’s typecheck script runs tsc --noEmit without generating those Next dev artifacts, causing
missing-module typecheck failures on clean checkouts/CI.
Code

next-env.d.ts[3]

+import "./.next/dev/types/routes.d.ts";
Relevance

⭐ Low

Same next-env change merged previously; team accepted .next/dev routes import (PR 281).

PR-#281

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new import targets a dev-only .next subpath, while the repo ignores .next/ and runs
typechecking via tsc --noEmit, so the imported .d.ts file won’t exist in clean environments.

next-env.d.ts[1-4]
.gitignore[19-22]
package.json[6-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`next-env.d.ts` imports a dev-only generated file (`.next/dev/types/routes.d.ts`). This path is not reliably present in CI or clean local checkouts, and will break `tsc --noEmit`.

### Issue Context
- `.next/` is gitignored, so it’s not guaranteed to exist at typecheck time.
- `npm run typecheck` runs plain `tsc --noEmit` (no `next dev/build` beforehand).

### Fix Focus Areas
- next-env.d.ts[1-3]

### Proposed fix
- Revert to the prior stable path (`./.next/types/routes.d.ts`) **or** remove the import if routes typing isn’t required for `tsc` runs.
- If typed routes are required, ensure the CI/typecheck pipeline generates the expected `.next/types/routes.d.ts` before running `tsc` (but avoid referencing `.next/dev/...`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. createAdminClient used in action 📘 Rule violation ⌂ Architecture
Description
resolveJoinCode uses createAdminClient from @/lib/supabase/admin instead of the required
await createClient() from @/lib/supabase/server. This bypasses the shared server-side Supabase
client factory requirement.
Code

src/app/join/actions.ts[R3-43]

+import { createAdminClient } from '@/lib/supabase/admin';
+
+export type ResolveJoinCodeResult =
+  | { ok: true; sport: 'golf' | 'baseball' }
+  | { ok: false; error: string };
+
+/**
+ * Sport-agnostic invite-code resolver for the public `/join` page.
+ *
+ * Every existing invite flow (`/golf/join/[code]`, `/baseball/join/[code]`)
+ * requires the visitor to be authenticated BEFORE it even looks the code
+ * up — by design, the underlying RLS policies (`golf_teams_select_by_join_code`,
+ * `baseball_teams_select`, "Anyone can view active invitations by code")
+ * are all `FOR SELECT TO authenticated`, with no anon grant. A single
+ * sport-agnostic field on a public marketing page can't tell which product
+ * a code belongs to through the normal RLS-scoped client (`@/lib/supabase/server`)
+ * — an anonymous visitor would see zero rows for every code, valid or not.
+ *
+ * This narrow, read-only, minimal-column lookup uses the existing
+ * service-role admin client (`@/lib/supabase/admin` — the same pattern
+ * already used by public token-based routes like
+ * `/api/calendar/feeds/[token]`) purely to answer "golf or baseball?". It
+ * returns nothing but that classification — never a team name, id, or
+ * roster — and performs zero writes. No new tables, no new migrations; the
+ * actual join (auth check, membership check, expiry check, confirmation
+ * UI) still happens entirely inside the existing per-sport `[code]` pages,
+ * untouched.
+ */
+export async function resolveJoinCode(rawCode: string): Promise<ResolveJoinCodeResult> {
+  const code = rawCode.trim().toUpperCase();
+
+  if (code.length < 4 || code.length > 12) {
+    return { ok: false, error: 'Enter the invite code your coach gave you.' };
+  }
+
+  const supabase = createAdminClient();
+
+  const [golfTeam, baseballTeam, baseballInvite] = await Promise.all([
+    supabase.from('golf_teams').select('id').eq('join_code', code).maybeSingle(),
+    supabase.from('baseball_teams').select('id').eq('join_code', code).maybeSingle(),
+    supabase.from('baseball_team_invitations').select('id').eq('code', code).maybeSingle(),
Relevance

⭐ Low

Prior review suggestion to remove createAdminClient usage was rejected; pattern appears tolerated
(PR 336).

PR-#336

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1519229 requires server-side Supabase clients to come from @/lib/supabase/server.
The server action imports and calls createAdminClient(), and that admin helper directly
instantiates a Supabase client via @supabase/supabase-js using environment variables.

Rule 1519229: Use the shared server-side Supabase client factory for all database access
src/app/join/actions.ts[3-43]
src/lib/supabase/admin.ts[1-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Server-side Supabase access in `resolveJoinCode` is performed via `createAdminClient()` (from `@/lib/supabase/admin`) rather than the mandated shared server helper `await createClient()` from `@/lib/supabase/server`.

## Issue Context
The compliance rule requires all server-only DB access paths (including server actions) to obtain Supabase clients exclusively via `@/lib/supabase/server`.

## Fix Focus Areas
- src/app/join/actions.ts[3-44]
- src/lib/supabase/server.ts[1-200]
- src/lib/supabase/admin.ts[1-21]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/app/join/actions.ts
@@ -0,0 +1,57 @@
'use server';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +173 to +176
<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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment thread src/app/join/page.tsx
Comment on lines +1 to +12
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 />;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +141 to +146
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +38 to +67
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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

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

Comment on lines +34 to +49
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%)',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

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

Fable Integrator and others added 3 commits July 2, 2026 15:52
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)
@njrini99-code

Copy link
Copy Markdown
Owner Author

🤖 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 /join surface, in sage & cream. +2,526 / −10 across 28 files. Stacked on batch/baseball-fixes.

Risk / areas: Mostly public landing presentation, but this PR also touches /join — a functional route. Confirm the join flow (code entry / redirect) is unchanged behaviorally and only reskinned.

Reviewers should watch: the /join route for any logic drift, portal-card links pointing at the right destinations, and the M4 grade not reading as a second deep band alongside M3.

⚠️ CI failing — Blocked. Red gates: Unit tests, Business contracts, Supabase lint + RLS, Playwright (chromium), Review Gate all; plus advisory lighthouse-preview. Do not merge until the required gates are green.

Posted automatically by the Helm Mission Control sweep · read-only triage, no changes made.

Fable Integrator and others added 5 commits July 2, 2026 16:16
…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
…r watermark, brass-on-dark (Amendment 3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fz1ajgmqcEp1yPEzjnXgY5
@njrini99-code
njrini99-code merged commit 43f519a into batch/baseball-fixes Jul 2, 2026
25 of 30 checks passed
@njrini99-code
njrini99-code deleted the feat/entry-world-portals branch July 2, 2026 20:53
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