ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
86 files +3943 -1190

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

public/assets/styles/stx-site.cssmodified+8-0
Changes to public/assets/styles/stx-site.css
@@ -1,1 +1,9 @@
11.lsu-skip{position:fixed;left:1rem;top:-5rem;z-index:200;background:#fff;color:#000;padding:1rem}.lsu-skip:focus{top:1rem}main:focus{outline:none}.nav-links{gap:clamp(12px,1.6vw,28px)}.race-portal{max-width:720px;margin:auto;padding:clamp(24px,5vw,64px);border:1px solid #ffffff24;background:#0d0d0f}.race-portal label{display:block;font:600 12px var(--lsu-body);text-transform:uppercase;letter-spacing:.12em;margin:1rem 0 .5rem}.race-portal input,.race-portal textarea,.race-portal select{display:block;width:100%;padding:1rem;background:#050505;border:1px solid #ffffff40;color:#fff;font:inherit}.race-portal .btn{margin-top:1.5rem}.race-portal [role=alert]{color:#ff928f}.race-portal h2{font:400 36px var(--lsu-display);text-transform:uppercase}.race-portal p{line-height:1.65}.race-tabs{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}.race-tabs button{background:transparent;color:#fff;border:0;border-bottom:2px solid #ffffff40;padding:.75rem;cursor:pointer}.race-tabs button[aria-selected=true]{border-color:#dd0f15}.race-portal .field-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.lsu-scroll-gallery{overflow:auto;scroll-snap-type:x mandatory}.lsu-scroll-gallery>*{scroll-snap-align:start}@media(max-width:700px){.race-portal .field-row{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}.lsu-globe-stage{position:relative;width:100%;height:100%;min-height:420px}.lsu-globe-fallback{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none}.lsu-event-dot{display:block;width:14px;height:14px;border-radius:50%;background:#dd0f15;border:2px solid white;box-shadow:0 0 0 6px #dd0f1540}.ts-map{background:transparent}.lsu-globe-stage .ts-map{position:absolute;inset:0}.lsu-globe-stage:has([data-ts-map])>.lsu-globe-fallback{visibility:hidden}.hour-dial .rest-ring{width:100%;height:auto;aspect-ratio:1;transform:rotate(-90deg)}@supports(animation-timeline:view()){.duelscroll{view-timeline-name:--duel;view-timeline-axis:block}.duel-fill{transform-origin:left;animation:lsu-grow linear both;animation-timeline:--duel;animation-range:entry 20% exit 65%}@keyframes lsu-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}}@media(prefers-reduced-motion:reduce){.duel-fill{animation:none}}.lsu-host-dialog[data-stx-dialog]{z-index:200}.lsu-host-dialog .sr-dialog{color:var(--lsu-white,#fff);background:#080808;padding:0;border:1px solid #333;border-radius:0}
2
3/* The page you are on, in the chrome every page shares. Nothing marked it
4 before, so the nav read the same on all twenty-five routes. A section
5 marks its own subtree, so a coaching module lights Coaching. Both a colour
6 and a rule, because colour alone is not a distinction everyone can see. */
7.nav-links.lsu-scope-nav a.lsu-scope-nav[aria-current=page]{color:var(--lsu-white);box-shadow:inset 0 -2px 0 0 var(--lsu-red)}
8.drawer-links.lsu-scope-nav a.lsu-scope-nav[aria-current=page]{color:var(--lsu-red)}
9.footer-col.lsu-sz7xmlte a.lsu-sz7xmlte[aria-current=page]{color:var(--lsu-white);text-decoration:underline;text-decoration-color:var(--lsu-red);text-underline-offset:4px}
resources/assets/styles/site/stx-site.cssmodified+8-0
Changes to resources/assets/styles/site/stx-site.css
@@ -1,1 +1,9 @@
11.lsu-skip{position:fixed;left:1rem;top:-5rem;z-index:200;background:#fff;color:#000;padding:1rem}.lsu-skip:focus{top:1rem}main:focus{outline:none}.nav-links{gap:clamp(12px,1.6vw,28px)}.race-portal{max-width:720px;margin:auto;padding:clamp(24px,5vw,64px);border:1px solid #ffffff24;background:#0d0d0f}.race-portal label{display:block;font:600 12px var(--lsu-body);text-transform:uppercase;letter-spacing:.12em;margin:1rem 0 .5rem}.race-portal input,.race-portal textarea,.race-portal select{display:block;width:100%;padding:1rem;background:#050505;border:1px solid #ffffff40;color:#fff;font:inherit}.race-portal .btn{margin-top:1.5rem}.race-portal [role=alert]{color:#ff928f}.race-portal h2{font:400 36px var(--lsu-display);text-transform:uppercase}.race-portal p{line-height:1.65}.race-tabs{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:2rem}.race-tabs button{background:transparent;color:#fff;border:0;border-bottom:2px solid #ffffff40;padding:.75rem;cursor:pointer}.race-tabs button[aria-selected=true]{border-color:#dd0f15}.race-portal .field-row{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.lsu-scroll-gallery{overflow:auto;scroll-snap-type:x mandatory}.lsu-scroll-gallery>*{scroll-snap-align:start}@media(max-width:700px){.race-portal .field-row{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}.lsu-globe-stage{position:relative;width:100%;height:100%;min-height:420px}.lsu-globe-fallback{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;pointer-events:none}.lsu-event-dot{display:block;width:14px;height:14px;border-radius:50%;background:#dd0f15;border:2px solid white;box-shadow:0 0 0 6px #dd0f1540}.ts-map{background:transparent}.lsu-globe-stage .ts-map{position:absolute;inset:0}.lsu-globe-stage:has([data-ts-map])>.lsu-globe-fallback{visibility:hidden}.hour-dial .rest-ring{width:100%;height:auto;aspect-ratio:1;transform:rotate(-90deg)}@supports(animation-timeline:view()){.duelscroll{view-timeline-name:--duel;view-timeline-axis:block}.duel-fill{transform-origin:left;animation:lsu-grow linear both;animation-timeline:--duel;animation-range:entry 20% exit 65%}@keyframes lsu-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}}@media(prefers-reduced-motion:reduce){.duel-fill{animation:none}}.lsu-host-dialog[data-stx-dialog]{z-index:200}.lsu-host-dialog .sr-dialog{color:var(--lsu-white,#fff);background:#080808;padding:0;border:1px solid #333;border-radius:0}
2
3/* The page you are on, in the chrome every page shares. Nothing marked it
4 before, so the nav read the same on all twenty-five routes. A section
5 marks its own subtree, so a coaching module lights Coaching. Both a colour
6 and a rule, because colour alone is not a distinction everyone can see. */
7.nav-links.lsu-scope-nav a.lsu-scope-nav[aria-current=page]{color:var(--lsu-white);box-shadow:inset 0 -2px 0 0 var(--lsu-red)}
8.drawer-links.lsu-scope-nav a.lsu-scope-nav[aria-current=page]{color:var(--lsu-red)}
9.footer-col.lsu-sz7xmlte a.lsu-sz7xmlte[aria-current=page]{color:var(--lsu-white);text-decoration:underline;text-decoration-color:var(--lsu-red);text-underline-offset:4px}
resources/components/SiteFooter.stxmodified+25-13
Changes to resources/components/SiteFooter.stx
@@ -1,3 +1,15 @@
1<script>
2interface Props { current?: string }
3const { current = '' } = defineProps<Props>()
4</script>
5<script server>
6/** Same rule as the nav: a section marks its own subtree. */
7function here(href) {
8 if (!current) return false
9 if (href === '/') return current === '/'
10 return current === href || current.startsWith(`${href}/`)
11}
12</script>
113<footer class="footer lsu-sz7xmlte">
214<div class="footer-grid lsu-sz7xmlte">
315<div class="footer-brand lsu-sz7xmlte">
@@ -8,13 +20,13 @@
820</div>
921<div class="footer-col lsu-sz7xmlte">
1022<div class="footer-head lsu-sz7xmlte">Race</div>
11<a href="/" class="lsu-sz7xmlte">Home</a>
12<a href="/format" class="lsu-sz7xmlte">The format</a>
13<a href="/world-series" class="lsu-sz7xmlte">World Series</a>
14<a href="/soul-races" class="lsu-sz7xmlte">Soul Races</a>
15<a href="/results" class="lsu-sz7xmlte">Results overview</a>
16<a href="/results/last-soul-ultra-2026" class="lsu-sz7xmlte">Season 2 results</a>
17<a href="/results/last-soul-ultra-2025" class="lsu-sz7xmlte">Season 1 results</a>
23<a href="/" class="lsu-sz7xmlte" @if(here('/')) aria-current="page" @endif>Home</a>
24<a href="/format" class="lsu-sz7xmlte" @if(here('/format')) aria-current="page" @endif>The format</a>
25<a href="/world-series" class="lsu-sz7xmlte" @if(here('/world-series')) aria-current="page" @endif>World Series</a>
26<a href="/soul-races" class="lsu-sz7xmlte" @if(here('/soul-races')) aria-current="page" @endif>Soul Races</a>
27<a href="/results" class="lsu-sz7xmlte" @if(here('/results')) aria-current="page" @endif>Results overview</a>
28<a href="/results/last-soul-ultra-2026" class="lsu-sz7xmlte" @if(here('/results/last-soul-ultra-2026')) aria-current="page" @endif>Season 2 results</a>
29<a href="/results/last-soul-ultra-2025" class="lsu-sz7xmlte" @if(here('/results/last-soul-ultra-2025')) aria-current="page" @endif>Season 1 results</a>
1830<a href="/world-series" data-apply data-view-href="/profile" class="lsu-sz7xmlte">Request a race entry</a>
1931</div>
2032<div class="footer-col lsu-sz7xmlte">
@@ -26,12 +38,12 @@
2638</div>
2739<div class="footer-col lsu-sz7xmlte">
2840<div class="footer-head lsu-sz7xmlte">Info</div>
29<a href="/sponsorship" class="lsu-sz7xmlte">Sponsorship</a>
30<a href="/coaching" class="lsu-sz7xmlte">Coaching</a>
31<a href="/contact" class="lsu-sz7xmlte">Contact &amp; FAQ</a>
32<a href="/imprint" class="lsu-sz7xmlte">Imprint</a>
33<a href="/privacy" class="lsu-sz7xmlte">Privacy</a>
34<a href="/terms" class="lsu-sz7xmlte">Terms</a>
41<a href="/sponsorship" class="lsu-sz7xmlte" @if(here('/sponsorship')) aria-current="page" @endif>Sponsorship</a>
42<a href="/coaching" class="lsu-sz7xmlte" @if(here('/coaching')) aria-current="page" @endif>Coaching</a>
43<a href="/contact" class="lsu-sz7xmlte" @if(here('/contact')) aria-current="page" @endif>Contact &amp; FAQ</a>
44<a href="/imprint" class="lsu-sz7xmlte" @if(here('/imprint')) aria-current="page" @endif>Imprint</a>
45<a href="/privacy" class="lsu-sz7xmlte" @if(here('/privacy')) aria-current="page" @endif>Privacy</a>
46<a href="/terms" class="lsu-sz7xmlte" @if(here('/terms')) aria-current="page" @endif>Terms</a>
3547</div>
3648</div>
3749<div class="footer-base lsu-sz7xmlte">
resources/components/SiteNav.stxmodified+36-16
Changes to resources/components/SiteNav.stx
@@ -1,3 +1,23 @@
1<script>
2interface Props {
3 /** The canonical path of the page this nav is rendering on. */
4 current?: string
5}
6const { current = '' } = defineProps<Props>()
7</script>
8<script server>
9/**
10 * Which link is the page you are on.
11 *
12 * A section marks its own subtree, so /coaching/calendar lights Coaching,
13 * and /faq canonicalises to /contact so Contact & FAQ is already right.
14 */
15function here(href) {
16 if (!current) return false
17 if (href === '/') return current === '/'
18 return current === href || current.startsWith(`${href}/`)
19}
20</script>
121<script client>
222const lsuMenu = state(false)
323function lsuToggleMenu() { lsuMenu.update(value => !value) }
@@ -8,16 +28,16 @@ function lsuCloseMenu() { lsuMenu.set(false) }
828<img src="/assets/lsu/images/lsu-logo.svg" alt="Last Soul Ultra" width="877" height="177" class="lsu-scope-nav">
929</a>
1030<div class="lsu-scope-nav nav-links">
11<a href="/" class="lsu-scope-nav">Home</a>
12<a href="/format" class="lsu-scope-nav">Format</a>
13<a href="/world-series" class="lsu-scope-nav">World Series</a>
14<a href="/soul-races" class="lsu-scope-nav">Soul Races</a>
15<a href="/sponsorship" class="lsu-scope-nav">Sponsorship</a>
16<a href="/coaching" class="lsu-scope-nav">Coaching</a>
17<a href="/contact" class="lsu-scope-nav">Contact &amp; FAQ</a>
31<a href="/" class="lsu-scope-nav" @if(here('/')) aria-current="page" @endif>Home</a>
32<a href="/format" class="lsu-scope-nav" @if(here('/format')) aria-current="page" @endif>Format</a>
33<a href="/world-series" class="lsu-scope-nav" @if(here('/world-series')) aria-current="page" @endif>World Series</a>
34<a href="/soul-races" class="lsu-scope-nav" @if(here('/soul-races')) aria-current="page" @endif>Soul Races</a>
35<a href="/sponsorship" class="lsu-scope-nav" @if(here('/sponsorship')) aria-current="page" @endif>Sponsorship</a>
36<a href="/coaching" class="lsu-scope-nav" @if(here('/coaching')) aria-current="page" @endif>Coaching</a>
37<a href="/contact" class="lsu-scope-nav" @if(here('/contact')) aria-current="page" @endif>Contact &amp; FAQ</a>
1838</div>
1939<div class="lsu-scope-nav nav-end">
20<a href="/profile" class="lsu-scope-nav nav-profile">Profile</a>
40<a href="/profile" class="lsu-scope-nav nav-profile" @if(here('/profile')) aria-current="page" @endif>Profile</a>
2141<a href="/world-series" class="lsu-scope-nav nav-cta" data-apply data-view-href="/profile" data-view-label="View entry">Request a race entry</a>
2242<button class="burger lsu-scope-nav" data-burger aria-label="Menu" :aria-expanded="lsuMenu" @click="lsuToggleMenu()" aria-controls="nav-drawer">
2343<span class="lsu-scope-nav">
@@ -35,14 +55,14 @@ function lsuCloseMenu() { lsuMenu.set(false) }
3555<aside :data-open="lsuMenu ? '' : null" :inert="!lsuMenu" @keydown.escape="lsuCloseMenu()" class="drawer lsu-scope-nav" id="nav-drawer" data-drawer :aria-hidden="!lsuMenu">
3656<div class="drawer-kicker lsu-scope-nav">Last Soul Ultra events</div>
3757<div class="drawer-links lsu-scope-nav">
38<a href="/" class="lsu-scope-nav">Home</a>
39<a href="/format" class="lsu-scope-nav">Format</a>
40<a href="/world-series" class="lsu-scope-nav">World Series</a>
41<a href="/soul-races" class="lsu-scope-nav">Soul Races</a>
42<a href="/sponsorship" class="lsu-scope-nav">Sponsorship</a>
43<a href="/coaching" class="lsu-scope-nav">Coaching</a>
44<a href="/contact" class="lsu-scope-nav">Contact &amp; FAQ</a>
45<a href="/profile" class="lsu-scope-nav">Profile</a>
58<a href="/" class="lsu-scope-nav" @if(here('/')) aria-current="page" @endif>Home</a>
59<a href="/format" class="lsu-scope-nav" @if(here('/format')) aria-current="page" @endif>Format</a>
60<a href="/world-series" class="lsu-scope-nav" @if(here('/world-series')) aria-current="page" @endif>World Series</a>
61<a href="/soul-races" class="lsu-scope-nav" @if(here('/soul-races')) aria-current="page" @endif>Soul Races</a>
62<a href="/sponsorship" class="lsu-scope-nav" @if(here('/sponsorship')) aria-current="page" @endif>Sponsorship</a>
63<a href="/coaching" class="lsu-scope-nav" @if(here('/coaching')) aria-current="page" @endif>Coaching</a>
64<a href="/contact" class="lsu-scope-nav" @if(here('/contact')) aria-current="page" @endif>Contact &amp; FAQ</a>
65<a href="/profile" class="lsu-scope-nav" @if(here('/profile')) aria-current="page" @endif>Profile</a>
4666</div>
4767<a href="/world-series" class="drawer-cta lsu-scope-nav" data-apply data-view-href="/profile" data-view-label="View entry">Request a race entry</a>
4868<div class="drawer-social lsu-scope-nav">
resources/layouts/LSU.stxmodified+2-2
Changes to resources/layouts/LSU.stx
@@ -4,9 +4,9 @@
44<body class="lsu-scope-site">
55 <a class="lsu-skip" href="#main">Skip to content</a>
66 <div class="lsu-scope-site page">
7 <SiteNav />
7 <SiteNav current="" />
88 <main id="main" tabindex="-1">@slot('content')</main>
9 <SiteFooter />
9 <SiteFooter current="" />
1010 </div>
1111</body>
1212</html>
tests/navigation.test.tsadded+65-0
Changes to tests/navigation.test.ts
@@ -0,0 +1,65 @@
1import { describe, expect, it } from 'bun:test'
2import { resolve } from 'node:path'
3import { processDirectives } from '@stacksjs/stx'
4
5/**
6 * The chrome every page shares marked nothing, so the navigation read the
7 * same on all twenty-five routes and no assistive technology could say which
8 * one you were on.
9 */
10
11async function render(view: string): Promise<string> {
12 const path = resolve(`resources/views/${view}.stx`)
13 return processDirectives(await Bun.file(path).text(), {}, path, {
14 enabled: true,
15 cache: false,
16 componentsDir: resolve('resources/components'),
17 layoutsDir: resolve('resources/layouts'),
18 partialsDir: resolve('resources/partials'),
19 }, new Set())
20}
21
22/** Every href marked as the current page, deduplicated. */
23async function marked(view: string): Promise<string[]> {
24 const html = await render(view)
25 return [...new Set([...html.matchAll(/<a href="([^"]+)"[^>]*aria-current="page"/g)].map(match => match[1]!))]
26}
27
28describe('knowing which page you are on', () => {
29 it('marks the page itself', async () => {
30 expect(await marked('format')).toEqual(['/format'])
31 expect(await marked('sponsorship')).toEqual(['/sponsorship'])
32 })
33
34 it('marks home only at the root, not on every path under it', async () => {
35 expect(await marked('index')).toEqual(['/'])
36 expect(await marked('format')).not.toContain('/')
37 })
38
39 it('marks the section a nested page belongs to', async () => {
40 expect(await marked('coaching/calendar')).toContain('/coaching')
41 expect(await marked('results/last-soul-ultra-2026')).toContain('/results')
42 })
43
44 it('follows the canonical, so an alias marks the page it points at', async () => {
45 // /faq renders the contact page and canonicalises to /contact.
46 expect(await marked('faq')).toEqual(await marked('contact'))
47 expect(await marked('faq')).toContain('/contact')
48 })
49
50 it('marks nothing on a page that is in neither nav', async () => {
51 expect(await marked('404')).toEqual([])
52 })
53
54 it('marks it in the drawer and the footer too, not just the bar', async () => {
55 const html = await render('format')
56 // Three copies of the Format link: the bar, the mobile drawer, the footer.
57 expect([...html.matchAll(/href="\/format"[^>]*aria-current="page"/g)].length).toBeGreaterThanOrEqual(3)
58 })
59
60 it('tells it apart by more than colour', async () => {
61 const css = await Bun.file('resources/assets/styles/site/stx-site.css').text()
62 expect(css).toContain('[aria-current=page]{color:var(--lsu-white);box-shadow')
63 expect(css).toContain('text-decoration:underline')
64 })
65})