Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

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

3. Dev-only routes type import 🐞 Bug ☼ Reliability

next-env.d.ts imports ./.next/dev/types/routes.d.ts, a generated dev artifact that won’t exist
in clean checkouts and may not exist after next build, causing tsc --noEmit (used by `npm run
typecheck`) to fail.
Agent Prompt
## Issue description
`next-env.d.ts` currently imports a generated file under `./.next/dev/types/`.

Because `.next/` is ignored and not guaranteed to exist (and build output paths differ between dev and build), this import can break `npm run typecheck` in CI/fresh checkouts.

## Issue Context
- The repo’s `typecheck` script is `tsc --noEmit` (no implicit Next build step).
- `.next/` is gitignored, so the imported file is not present reliably.

## Fix Focus Areas
- next-env.d.ts[1-6]
- tsconfig.json[58-65]
- package.json[6-36]
- .gitignore[19-22]

## Suggested fix
- Remove the `import "./.next/dev/types/routes.d.ts";` line from `next-env.d.ts`.
- If typed routes are still desired, include them via `tsconfig.json` patterns that do **not** hard-fail when `.next/` is missing (e.g., include `.next/**/types/**/*.d.ts`), or ensure CI generates `.next` types before `tsc` runs (explicit pre-step).

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


// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Binary file added public/marketing/first-light/photos/baseball.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/marketing/first-light/photos/golf.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/marketing/first-light/photos/hero.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/marketing/first-light/photos/mist.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 35 additions & 9 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import type { Metadata } from 'next'
import { Hero } from '@/components/landing/Hero'
import { Footer } from '@/components/landing/Footer'
import { SmoothScroll } from '@/components/landing/SmoothScroll'
import { NativeRedirect } from '@/components/NativeRedirect'
import { LenisRoot } from '@/components/marketing/first-light/scroll/LenisRoot'
import { M1Hero } from '@/components/marketing/first-light/moments/M1Hero'
import { M2Clarity } from '@/components/marketing/first-light/moments/M2Clarity'
import { M3ProductCinema } from '@/components/marketing/first-light/moments/M3ProductCinema'
import { M4TwoFields } from '@/components/marketing/first-light/moments/M4TwoFields'
import { M5Intelligence } from '@/components/marketing/first-light/moments/M5Intelligence'
import { M6ForThePlayer } from '@/components/marketing/first-light/moments/M6ForThePlayer'
import { M7Honesty } from '@/components/marketing/first-light/moments/M7Honesty'
import { M8FinalCTA } from '@/components/marketing/first-light/moments/M8FinalCTA'
import { M9Footer } from '@/components/marketing/first-light/moments/M9Footer'
import '@/components/marketing/first-light/first-light.css'

export const metadata: Metadata = {
title: 'Helm Sports Labs',
Expand All @@ -29,18 +37,36 @@ export const metadata: Metadata = {
},
}

// `overflow-x-clip`, NOT `overflow-x-hidden`, below is load-bearing (see
// the same documented gotcha in src/app/globals.css ~L144): the CSS
// Overflow spec promotes overflow-y to `auto` whenever overflow-x is a
// {hidden,scroll,auto} value and overflow-y is a {visible,clip} value —
// turning <main> into its own scroll container. That breaks every
// `position: sticky` descendant (PinnedScrub/M3) — sticky then pins
// relative to <main>'s own (never-scrolling) frame instead of the real
// window scroll. `clip` pairs with `visible` (both in the exempt group),
// so no promotion happens and sticky works, while still clipping the
// horizontal overflow the M1/M4/M8 full-bleed sections can produce.
export default function LandingPage() {
return (
<main className="min-h-dvh overflow-x-hidden bg-stone-950">
<main className="min-h-dvh overflow-x-clip" style={{ backgroundColor: 'var(--fl-pine)' }}>
<NativeRedirect to="/golf/login" />
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary-600 focus:text-white focus:rounded-lg focus:shadow-lg">
Skip to main content
</a>
<SmoothScroll />
<div id="main-content">
<Hero />
</div>
<Footer />
<LenisRoot>
<div id="main-content">
<M1Hero />
<M2Clarity />
<M3ProductCinema />
<M4TwoFields />
<M5Intelligence />
<M6ForThePlayer />
<M7Honesty />
<M8FinalCTA />
</div>
<M9Footer />
</LenisRoot>
</main>
)
}
415 changes: 415 additions & 0 deletions src/components/marketing/first-light/CONTRACTS.md

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions src/components/marketing/first-light/brand/HelmMark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getSvgA11yProps } from './svgA11y';

export interface HelmMarkProps {
/** Rendered width/height in px. viewBox is a fixed 24×24 unit square, so
* the 1.5px stroke visually thins as `size` shrinks below 24 — this mark
* is meant for 18px+ use (GlassNav, M9 watermark); for 10–14px punctuation
* use `HelmRosette` instead, which holds its stroke weight at small sizes. */
size?: number;
className?: string;
/** Accessible name. Omit for a purely decorative mark (aria-hidden). */
title?: string;
}

const CX = 12;
const CY = 12;
const RING_R = 7.4;
const SPOKE_INNER_R = 6.6;
const SPOKE_OUTER_R = 10.4;
// 8 spokes, 45° apart, starting at 12 o'clock — matches Helm-Logo-New-Main.png.
const SPOKE_ANGLES_DEG = [0, 45, 90, 135, 180, 225, 270, 315];

function pointOnCircle(angleDeg: number, r: number) {
const rad = (angleDeg * Math.PI) / 180;
return { x: CX + r * Math.sin(rad), y: CY - r * Math.cos(rad) };
}

/**
* The Helm mark — a faithful line-art tracing of the 8-spoke ship's-helm
* wheel + ship silhouette in `public/Helm-Logo-New-Main.png`. Stroke-based
* (`fill="none"`, `stroke="currentColor"`) so it recolors per surface
* (sage-ink on cream, brass as jewelry, cream on sage-ink bands) instead of
* carrying that PNG's flat kelly fill.
*
* Kelly raster (`Helm-Logo-New-Main.png` and the sport-variant PNGs) NEVER
* renders on landing/auth chrome — this component (plus `HelmRosette` and
* `SportGlyph`) is the only brand-mark rendering path there. See
* CONTRACTS.md "Brand components" for the full consumer map.
*
* Ship silhouette, top to bottom: mast crossbar (small rect) → deckhouse
* cabin (wider rect) → hull bow (triangular path, `strokeLinejoin="round"`
* softens the top corners toward the source PNG's pentagon read) →
* waterline (a shallow arc wider than the hull).
*/
export function HelmMark({ size = 24, className, title }: HelmMarkProps) {
const a11y = getSvgA11yProps({ title });
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
className={className}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
{...a11y}
>
{title ? <title>{title}</title> : null}
{SPOKE_ANGLES_DEG.map((angle) => {
const inner = pointOnCircle(angle, SPOKE_INNER_R);
const outer = pointOnCircle(angle, SPOKE_OUTER_R);
return <line key={angle} x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} />;
})}
<circle cx={CX} cy={CY} r={RING_R} />
{/* mast crossbar */}
<rect x={10.4} y={7.8} width={3.2} height={1} rx={0.5} />
{/* deckhouse cabin */}
<rect x={9.4} y={9.1} width={5.2} height={1.3} rx={0.65} />
{/* hull (bow silhouette) */}
<path d="M8.6,10.6 L15.4,10.6 L12,14.2 Z" />
{/* waterline */}
<path d="M7.6,14 Q12,15.6 16.4,14" />
</svg>
);
}
68 changes: 68 additions & 0 deletions src/components/marketing/first-light/brand/HelmRosette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getSvgA11yProps } from './svgA11y';

export interface HelmRosetteProps {
size?: number;
className?: string;
title?: string;
}

const CX = 12;
const CY = 12;
const RING_R = 7.4;
const SPOKE_INNER_R = 6.6;
const SPOKE_OUTER_R = 10.4;
// Same wheel geometry as HelmMark — the two components read as one mark at
// different scales.
const SPOKE_ANGLES_DEG = [0, 45, 90, 135, 180, 225, 270, 315];

function pointOnCircle(angleDeg: number, r: number) {
const rad = (angleDeg * Math.PI) / 180;
return { x: CX + r * Math.sin(rad), y: CY - r * Math.cos(rad) };
}

/**
* The Helm wheel reduced to ring + 8 spokes — no ship — for 10–14px section
* punctuation (Amendment 3 §A.2): the eyebrow hairlines of M2/M5/M7 meet a
* brass rosette at center instead of bare line ends; M6's "readiness"
* vignette gets a neutral (sage) rosette per §B.3.
*
* Draws with `vector-effect="non-scaling-stroke"` — a fixed screen-pixel
* stroke regardless of the viewBox→viewport scale. At 10–14px a stroke that
* scales down with the icon (HelmMark's default behavior, correct at 18px+)
* would thin into anti-aliasing mush; a rosette this small needs a constant
* hairline instead, so the base `strokeWidth` here (1.25) is intentionally
* lower than HelmMark's 1.5 — it's the literal on-screen weight at every size.
*/
export function HelmRosette({ size = 12, className, title }: HelmRosetteProps) {
const a11y = getSvgA11yProps({ title });
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
className={className}
fill="none"
stroke="currentColor"
strokeWidth={1.25}
strokeLinecap="round"
{...a11y}
>
{title ? <title>{title}</title> : null}
{SPOKE_ANGLES_DEG.map((angle) => {
const inner = pointOnCircle(angle, SPOKE_INNER_R);
const outer = pointOnCircle(angle, SPOKE_OUTER_R);
return (
<line
key={angle}
x1={inner.x}
y1={inner.y}
x2={outer.x}
y2={outer.y}
vectorEffect="non-scaling-stroke"
/>
);
})}
<circle cx={CX} cy={CY} r={RING_R} vectorEffect="non-scaling-stroke" />
</svg>
);
}
84 changes: 84 additions & 0 deletions src/components/marketing/first-light/brand/SportGlyph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getSvgA11yProps } from './svgA11y';

export type SportGlyphSport = 'golf' | 'baseball' | 'lift';

export interface SportGlyphProps {
sport: SportGlyphSport;
size?: number;
className?: string;
title?: string;
}

/**
* Tiny per-sport line glyphs, 16–20px by default (Amendment 3 §B). Same
* stroke contract as HelmMark/HelmRosette: `fill="none"`,
* `stroke="currentColor"`, shared 24×24 viewBox so all three brand
* components compose at consistent relative scale.
*
* - **golf** — flag-on-horizon: flagstick + pennant + ground line, plus a
* small ringed dot at the base standing in for the golf-ball motif on
* `public/helm-golf-logo-transparent.png`'s wheel center. Echoes M4's
* "horizon + flag + green-contour arcs" background motif language (§B.1)
* at icon scale — that larger background motif is a separate asset the
* `landing-portals+cta` lane builds; this glyph is the small icon variant
* for portal cards / vignette headers (§B.2, §B.3), NOT that background.
* - **baseball** — the chalk foul-line V + base-path arc, the
* "groundskeeper's blueprint" geometry described in
* `docs/baseball/ENTRY_SCENES_DESIGN.md`. That doc's literal
* `EntryField.tsx` scene component doesn't exist in this branch yet (it
* belongs to the not-yet-built `entry-scenes` lane) — this geometry is
* derived from the design doc's description (two foul lines converging at
* home plate + a shallow infield arc), not read from that file. If
* `EntryField.tsx` lands with different exact angles, reconcile the two.
* - **lift** — barbell: bar + two concentric plate circles per sport, echoing
* an `IronRoomField` scene component that also doesn't exist yet in this
* branch; built from Amendment 3 §B's literal recipe instead.
*
* Consumers may also render this at much larger sizes (via `size`) at low
* opacity for a decorative background motif rather than an icon — the paths
* stay proportionate at any scale since everything is defined in the shared
* 24-unit viewBox.
*/
export function SportGlyph({ sport, size = 18, className, title }: SportGlyphProps) {
const a11y = getSvgA11yProps({ title });
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
className={className}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
{...a11y}
>
{title ? <title>{title}</title> : null}
{sport === 'golf' && (
<>
<path d="M4,17 H20" />
<path d="M9,17 V6" />
<path d="M9,6 L15,8.2 L9,10.4 Z" />
<circle cx={6.3} cy={17} r={1.1} />
</>
)}
{sport === 'baseball' && (
<>
<path d="M12,19 L4,5" />
<path d="M12,19 L20,5" />
<path d="M8.4,12.7 Q12,10.9 15.6,12.7" />
</>
)}
{sport === 'lift' && (
<>
<path d="M2,12 H22" />
<circle cx={6} cy={12} r={3.4} />
<circle cx={6} cy={12} r={1.6} />
<circle cx={18} cy={12} r={3.4} />
<circle cx={18} cy={12} r={1.6} />
</>
)}
</svg>
);
}
11 changes: 11 additions & 0 deletions src/components/marketing/first-light/brand/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Brand components (Amendment 3 §A/§B) — see CONTRACTS.md "Brand components"
* for the full consumer map. Re-exported from the top-level first-light
* `index.ts`; import from there in moment files, not from this path directly.
*/
export { HelmMark } from './HelmMark';
export type { HelmMarkProps } from './HelmMark';
export { HelmRosette } from './HelmRosette';
export type { HelmRosetteProps } from './HelmRosette';
export { SportGlyph } from './SportGlyph';
export type { SportGlyphProps, SportGlyphSport } from './SportGlyph';
25 changes: 25 additions & 0 deletions src/components/marketing/first-light/brand/svgA11y.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Shared accessibility contract for the brand SVG icon family (HelmMark,
* HelmRosette, SportGlyph). Decorative by default — `aria-hidden` +
* `focusable="false"` (the latter kills the old-Edge/IE11 "SVGs are
* keyboard-focusable by default" quirk) — and becomes a labeled image
* (`role="img"` + `aria-label` + a `<title>` child the caller renders)
* only when a `title` is supplied.
*/
export interface SvgA11yInput {
title?: string;
}

export interface SvgA11yProps {
role?: 'img';
'aria-hidden'?: boolean;
'aria-label'?: string;
focusable: 'false';
}

export function getSvgA11yProps({ title }: SvgA11yInput): SvgA11yProps {
if (title) {
return { role: 'img', 'aria-label': title, focusable: 'false' };
}
return { 'aria-hidden': true, focusable: 'false' };
}
Loading
Loading