Files
Portfolio/docs/superpowers/plans/2026-09-02-portfolio-nightdrive-redesign-implementation.md

45 KiB

Portfolio Nightdrive Redesign Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Rebuild Luca's portfolio as a cinematic, responsive chase-view night drive with a credible professional information layer, while preserving semantic rendering, accessibility, lifecycle safety, adaptive quality, and native scrolling.

Architecture: Keep the current Vue, Vite, TypeScript, Three.js, Zod, pre-render, and progressive-enhancement foundations. Consolidate scroll work behind one request-animation-frame clock, make section signs a deterministic time-based state machine, rebuild the procedural world from focused scene modules, and recompose the semantic document with explicit section layouts. Treat the information layer, scene, audio, and media pipeline as separate subsystems joined only through typed snapshots.

Tech Stack: Vue 3, Vite 8, TypeScript 5.9, Three.js 0.185, Web Audio, Zod 4, Vitest, Playwright, native CSS, Geist Variable, Geist Mono, Phosphor Vue, Simple Icons

Spec: docs/superpowers/specs/2026-09-02-portfolio-nightdrive-redesign-design.md

Global Constraints

  • Work only on branch codex/portfolio-nightdrive-redesign in /home/luca/Documents/Portfolio/.worktrees/portfolio-nightdrive-redesign.
  • Preserve native scrolling, canonical section IDs, pre-rendered semantic HTML, keyboard navigation, reduced motion, Static mode, print rendering, no-JavaScript content, and failure fallbacks.
  • Keep DESIGN_VARIANCE: 8, MOTION_INTENSITY: 8, and VISUAL_DENSITY: 4.
  • Keep the page dark-only. Cyan is the only interface accent; magenta and amber remain environmental light colors.
  • Use Geist Variable and Geist Mono. Visible copy contains no em dash or en dash characters.
  • Use one 12px information-surface radius and 10px control radius. Do not turn every nested group into a card.
  • Keep the car at the stable lower center and protect the horizon, vehicle, and lower controls from information-panel overlap.
  • Create no audio graph before an explicit activating gesture. Preserve visibility, interruption, BFCache, failure, and teardown behavior.
  • Use only authentic project media with known provenance. Do not generate or invent project screenshots. The final media acceptance gate remains open until Luca supplies or explicitly approves source media.
  • Primary agent owns all visual direction, Three.js files, information styling, responsive integration, and browser iteration.
  • Reuse the single Luna-high subagent only for isolated non-visual work. It must not edit visual files.
  • Run one quick review after the integrated scene, car, sign, and HUD milestone, then one final integrated review.
  • Do not work on deployment, release signing, Search Console, advanced SEO, or release infrastructure.

File Structure

New focused modules

  • src/components/HeroJourney.vue: semantic first-screen identity, role, value proposition, and Explore work action.
  • src/components/IconGlyph.vue: Phosphor interface-icon wrapper with one standard size and weight contract.
  • src/components/BrandIcon.vue: Simple Icons renderer for verified technology and social brand marks.
  • src/icons/brand-icons.ts: exact, exhaustive registry for toolkit and social labels present in validated content.
  • src/scene/post-processing.ts: EffectComposer, render pass, restrained bloom, resize, render, and disposal ownership.
  • src/scene/world/atmosphere.ts: deterministic stars, horizon haze, and acceleration streaks.
  • src/scene/world/palette.ts: chapter color targets and interpolation helpers shared by sky, sun, fog, and terrain.
  • src/audio/music-sequencer.ts: fixed-step procedural composition and Web Audio look-ahead scheduling.
  • tests/unit/world-models.test.ts: geometry, uniforms, canonical labels, responsive car bounds, and deterministic palette tests.
  • tests/unit/brand-icons.test.ts: exhaustive content-to-icon registry coverage.
  • docs/third-party-notices.md: Geist, Phosphor, Simple Icons, and any substantially adapted MIT reference notice.

Existing modules with changed responsibilities

  • src/composables/useScrollDrive.ts: the only raw window scroll listener and the shared frame-subscriber clock.
  • src/composables/useJourneySections.ts: structural measurement, semantic section selection, and sign-controller sampling without a raw scroll listener.
  • src/journey/sign-controller.ts: stateful, clock-driven sign lifecycle.
  • src/scene/world/road.ts: shader road, converging edges, moving center energy, and acceleration response.
  • src/scene/world/terrain.ts: deterministic displaced indexed line terrain with a road-center mask.
  • src/scene/world/sun.ts: scan-banded procedural sun with halo and route warmth.
  • src/scene/world/car.ts: original wide grand-tourer silhouette and damped response.
  • src/scene/world/sign.ts: physical roadside sign with canonical plain labels.
  • src/scene/world/environment.ts: orchestration only; construction, frame snapshots, resize, and chapter interpolation.
  • src/components/DriveControls.vue: inviting pre-activation state, collapsed running state, and settings popover.
  • src/components/JourneyHud.vue: compact route and section display without exposed quality text.
  • src/components/AppShell.vue: explicit semantic composition and subsystem wiring.
  • src/styles/*.css: tokens, information layouts, controls, world fallback, responsive states, and print overrides.

Task 1: Install and lock the visual foundation

Files:

  • Modify: package.json
  • Modify: package-lock.json
  • Modify: src/App.vue
  • Modify: src/styles/tokens.css
  • Modify: src/styles/base.css
  • Create: docs/third-party-notices.md
  • Test: tests/unit/toolchain.test.ts

Interfaces:

  • Consumes: existing Vite CSS import order and Vue application entry.

  • Produces: Geist Variable, Geist Mono, Phosphor Vue components, Simple Icons exports, and stable CSS tokens used by every subsequent task.

  • Step 1: Write dependency and typography assertions

Add assertions that exact direct dependencies exist and that the application imports the two font stylesheets before local CSS:

expect(packageJson.dependencies).toMatchObject({
  '@fontsource-variable/geist': '5.3.0',
  '@fontsource-variable/geist-mono': '5.3.0',
  '@phosphor-icons/vue': '2.2.1',
  'simple-icons': '16.29.0',
});
expect(appSource).toContain("@fontsource-variable/geist/wght.css");
expect(appSource).toContain("@fontsource-variable/geist-mono/wght.css");
  • Step 2: Run the focused test and verify it fails

Run: npx vitest run tests/unit/toolchain.test.ts

Expected: FAIL because the dependencies and font imports are absent.

  • Step 3: Install verified dependencies and import fonts

Run:

npm install --save-exact @fontsource-variable/geist@5.3.0 @fontsource-variable/geist-mono@5.3.0 @phosphor-icons/vue@2.2.1 simple-icons@16.29.0

Add before local CSS in src/App.vue:

import '@fontsource-variable/geist/wght.css';
import '@fontsource-variable/geist-mono/wght.css';

Define the stable visual tokens:

:root {
  color-scheme: dark;
  --canvas: #040814;
  --surface: rgb(7 14 25 / 95%);
  --surface-strong: rgb(7 14 25 / 97%);
  --surface-soft: rgb(15 28 43 / 92%);
  --line: rgb(126 200 214 / 22%);
  --line-strong: rgb(126 220 232 / 44%);
  --text: #f3f7f8;
  --text-muted: #aab9c2;
  --accent: #78d8e5;
  --focus: #ffd693;
  --world-magenta: #c74b91;
  --world-amber: #f4ad67;
  --radius-surface: 12px;
  --radius-control: 10px;
  --sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
  --mono: 'Geist Mono Variable', ui-monospace, monospace;
  --z-world: 0;
  --z-atmosphere: 1;
  --z-content: 2;
  --z-navigation: 3;
  --z-hud: 4;
  --z-popover: 5;
}

Record the package names, versions, licenses, copyright owners, and source URLs in docs/third-party-notices.md.

  • Step 4: Run the focused test and static verification

Run: npx vitest run tests/unit/toolchain.test.ts && npm run typecheck

Expected: PASS.

  • Step 5: Commit the foundation
git add package.json package-lock.json src/App.vue src/styles/tokens.css src/styles/base.css docs/third-party-notices.md tests/unit/toolchain.test.ts
git commit -m "feat: establish nightdrive visual foundation"

Task 2: Consolidate the animation clock and implement the sign state machine

Files:

  • Modify: src/journey/types.ts
  • Modify: src/journey/sign-controller.ts
  • Modify: src/composables/useScrollDrive.ts
  • Modify: src/composables/useJourneySections.ts
  • Modify: tests/unit/sign-controller.test.ts
  • Modify: tests/unit/scroll-drive-controller.test.ts
  • Modify: tests/browser/journey-integration.spec.ts

Interfaces:

  • Consumes: ScrollDriveSnapshot, NavigationState, canonical section IDs, heading document geometry, and requestAnimationFrame.
  • Produces: ScrollDriveBinding, SignController, and a JourneySnapshot whose sign phase advances by elapsed time rather than document-distance mapping.

Define these exact public contracts in src/journey/types.ts:

export type SignPhase = 'anticipate' | 'approach' | 'dwell' | 'depart';

export interface SignSnapshot {
  targetId: SectionId;
  phase: SignPhase;
  progress: number;
  opacity: number;
}

export interface SignControllerSample {
  now: number;
  source: JourneySource;
  direction: -1 | 0 | 1;
  acceleration: number;
  crossedBoundaries: number;
  candidateId: SectionId | null;
  candidateDistanceVh: number | null;
  activeSectionId: SectionId;
  motionAllowed: boolean;
}

export interface SignController {
  sample(input: SignControllerSample): SignSnapshot | null;
  reset(now?: number): void;
  needsFrame(): boolean;
}

Define the browser-facing binding in src/composables/useScrollDrive.ts, where Vue's Ref type belongs:

export interface ScrollDriveBinding {
  snapshot: Readonly<Ref<ScrollDriveSnapshot>>;
  requestFrame(): void;
  subscribeFrame(listener: (now: number, snapshot: ScrollDriveSnapshot) => boolean): () => void;
}
  • Step 1: Replace distance-mapping tests with lifecycle tests

Test the exact transition schedule:

const controller = createSignController();
expect(controller.sample(sample({ now: 0 }))).toMatchObject({ phase: 'anticipate', progress: 0 });
expect(controller.sample(sample({ now: 300 }))).toMatchObject({ phase: 'approach', progress: 0 });
expect(controller.sample(sample({ now: 1_200 }))).toMatchObject({ phase: 'dwell', progress: 1 });
expect(controller.sample(sample({ now: 2_599, activeSectionId: 'profile' }))).toMatchObject({ phase: 'dwell' });
expect(controller.sample(sample({ now: 2_600, activeSectionId: 'profile' }))).toMatchObject({ phase: 'depart', progress: 0 });
expect(controller.sample(sample({ now: 3_300, activeSectionId: 'profile' }))).toBeNull();
expect(controller.sample(sample({ now: 3_649, activeSectionId: 'profile' }))).toBeNull();
expect(controller.sample(sample({ now: 3_650, activeSectionId: 'profile', candidateId: 'experience' }))).toMatchObject({
  targetId: 'experience',
  phase: 'anticipate',
});

Also assert a 650ms accelerated approach, 350ms relatch cooldown, reverse cancellation before dwell, clean reverse departure during dwell, and immediate clearing for explicit navigation, history restoration, multi-section jumps, Static mode, and reduced motion.

  • Step 2: Run sign and scroll tests and verify failure

Run: npx vitest run tests/unit/sign-controller.test.ts tests/unit/scroll-drive-controller.test.ts

Expected: FAIL because createSignController and ScrollDriveBinding do not exist.

  • Step 3: Implement the stateful sign controller

Use constants with no geometry-derived phase mapping:

const ANTICIPATE_MS = 300;
const APPROACH_MS = 900;
const FAST_APPROACH_MS = 650;
const DWELL_MS = 1_400;
const DEPART_MS = 700;
const COOLDOWN_MS = 350;
const ELIGIBLE_DISTANCE_VH = 1.2;
const JUMP_DISTANCE_VH = 0.75;

Latch only forward passive-scroll candidates within 0 < candidateDistanceVh <= 1.2. Keep the target stable through the visible lifecycle. Require both the 1400ms dwell and target activation before departure. Return needsFrame() === false while a completed minimum dwell waits for target activation, then resume frames on the next scroll sample. Internally retain cooldown timing while returning null after departure.

  • Step 4: Turn useScrollDrive into the shared frame bus

Keep its one passive window.scroll listener. Run subscribers after each sampled drive frame and continue requesting frames while acceleration is nonzero or any subscriber returns true:

const keepRunning = [...subscribers].some((listener) => listener(now, nextSnapshot));
if (nextSnapshot.acceleration > 0 || keepRunning) scheduleFrame();

Expose snapshot, requestFrame, and subscribeFrame; remove no visibility, resize, or teardown protections.

  • Step 5: Remove the raw scroll listener from useJourneySections

Cache heading document positions when ResizeObserver, media load, orientation, viewport resize, or document.fonts.ready marks structure dirty. On each shared frame, recompute activation from cached headings and current scroll, sample the sign controller, and return signController.needsFrame() to keep the clock alive only during active transitions.

  • Step 6: Verify one scroll owner and lifecycle behavior

Run:

npx vitest run tests/unit/sign-controller.test.ts tests/unit/scroll-drive-controller.test.ts
npx playwright test tests/browser/journey-integration.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS, with exactly one production window.addEventListener('scroll', ...) path in the two composables.

  • Step 7: Commit the clock and sign model
git add src/journey src/composables/useScrollDrive.ts src/composables/useJourneySections.ts tests/unit/sign-controller.test.ts tests/unit/scroll-drive-controller.test.ts tests/browser/journey-integration.spec.ts
git commit -m "feat: drive signs from a shared animation clock"

Task 3: Rebuild road and displaced terrain

Files:

  • Modify: src/scene/render-quality.ts
  • Modify: src/scene/world/road.ts
  • Modify: src/scene/world/terrain.ts
  • Create: src/scene/world/palette.ts
  • Create: tests/unit/world-models.test.ts
  • Modify: tests/unit/scene-resources.test.ts

Interfaces:

  • Consumes: SceneResourceBag, RenderQualitySettings, drive distance, speed, acceleration, route progress, and chapter.
  • Produces: RoadWorld and TerrainWorld, each with group, update, and no independent scheduler.
export interface RoadWorld {
  group: THREE.Group;
  update(distance: number, speed: number, acceleration: number): void;
}

export interface TerrainWorld {
  group: THREE.Group;
  update(distance: number, speed: number, chapter: EnvironmentChapter, routeProgress: number): void;
}
  • Step 1: Write deterministic quality and world-model tests

Assert monotonic geometry and effects:

expect(high.terrainSegments).toBeGreaterThan(medium.terrainSegments);
expect(medium.terrainSegments).toBeGreaterThan(low.terrainSegments);
expect(high.streakCount).toBeGreaterThan(low.streakCount);
expect(low.postEffects).toBe(false);

Create road and terrain with a SceneResourceBag, assert a wide indexed line terrain, nonempty shader uniforms, a road-center displacement mask, and stable allocations after repeated update calls.

  • Step 2: Run the world tests and verify failure

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts

Expected: FAIL because the new settings and world contracts are absent.

  • Step 3: Implement quality-aware road geometry and shading

Use a subdivided plane extending from near the camera to the horizon. The road shader darkens the center, lights cyan outer edges, emits a narrow amber center lane, scrolls lane energy by distance, and slightly increases edge luminance with acceleration. Keep all movement in uniforms.

uniforms.uDistance.value = distance;
uniforms.uSpeed.value = speed;
uniforms.uAcceleration.value = acceleration;
  • Step 4: Implement deterministic indexed line terrain

Build one wide grid spanning both sides of the road. Displace height using seeded multi-octave value noise, flatten the road corridor with smoothstep, suppress hidden cross-road segments, and color near lines cyan with magenta at higher ridges. Recycle depth by a distance uniform rather than reallocating geometry.

  • Step 5: Verify world models and GPU ledger

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts && npm run typecheck

Expected: PASS with no allocation count increase across 300 updates.

  • Step 6: Commit road and terrain
git add src/scene/render-quality.ts src/scene/world/road.ts src/scene/world/terrain.ts src/scene/world/palette.ts tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts
git commit -m "feat: rebuild the nightdrive road and terrain"

Task 4: Build the layered sky, sun, atmosphere, and restrained post pass

Files:

  • Modify: src/scene/world/sun.ts
  • Delete: src/scene/world/particles.ts
  • Create: src/scene/world/atmosphere.ts
  • Create: src/scene/post-processing.ts
  • Modify: src/scene/three.d.ts
  • Modify: src/scene/world/environment.ts
  • Modify: src/styles/world.css
  • Modify: tests/unit/world-models.test.ts
  • Modify: tests/unit/scene-resources.test.ts

Interfaces:

  • Consumes: chapter palette, route progress, quality settings, viewport dimensions, drive distance, speed, and acceleration.
  • Produces: SunWorld, AtmosphereWorld, and PostProcessor with deterministic resize, render, and disposal behavior.
export interface PostProcessor {
  resize(width: number, height: number, pixelRatio: number): void;
  render(scene: THREE.Scene, camera: THREE.Camera): void;
  dispose(): void;
}
  • Step 1: Extend tests for sun bands, atmosphere counts, and disposal

Assert that sun materials contain a scan-band uniform, atmosphere object counts follow quality settings, chapter warmth is monotonic, and composer resources dispose exactly once.

  • Step 2: Run focused tests and verify failure

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts

Expected: FAIL for the missing atmosphere and post-processing contracts.

  • Step 3: Implement the procedural sun and halo

Use a shader disc with horizontal scan bands cut from its lower portion, a soft halo sprite generated from a small owned canvas texture, and chapter-driven color interpolation from magenta night to amber pre-dawn. Clamp luminance changes so no full-screen flash can occur.

  • Step 4: Implement stars, haze, and speed streaks

Use seeded points for stars, two broad transparent horizon planes for haze, and quality-capped line segments for streaks. Keep streak opacity near zero at cruise and raise it smoothly with acceleration.

  • Step 5: Replace the vignette target with restrained bloom

Use Three.js EffectComposer, RenderPass, and UnrealBloomPass. Set bloom threshold high enough that only road energy, rear lights, signs, and the sun bloom. Own composer and passes in SceneResourceBag; low quality renders directly without post-processing.

Declare the three .js example-module paths in src/scene/three.d.ts so the existing intentionally isolated untyped Three.js boundary remains contained.

  • Step 6: Integrate chapter interpolation and a matching static poster

Update environment.ts to lerp scene background, fog, sun, and terrain targets instead of switching hex colors in one frame. Rebuild .journey-poster with layered CSS road convergence, grid ridges, scan-banded sun, and a simplified car silhouette for Static, reduced-motion, loading, and failure states.

  • Step 7: Verify scene lifecycle and visual safety tests

Run:

npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts tests/unit/scene-lifecycle.test.ts
npm run typecheck

Expected: PASS.

  • Step 8: Commit sky and atmosphere
git add src/scene src/styles/world.css tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts
git commit -m "feat: layer the nightdrive sky and atmosphere"

Task 5: Reconstruct the original chase-view grand tourer

Files:

  • Modify: src/scene/world/car.ts
  • Modify: src/scene/world/environment.ts
  • Modify: tests/unit/world-models.test.ts
  • Modify: tests/unit/scene-resources.test.ts

Interfaces:

  • Consumes: speed, acceleration, elapsed seconds, viewport aspect, and responsive car-detail mode.
  • Produces: CarWorld with a stable group and damped update state.
export interface CarWorld {
  group: THREE.Group;
  setDetail(detail: 'full' | 'reduced'): void;
  update(speed: number, acceleration: number, elapsedSeconds: number): void;
}
  • Step 1: Strengthen silhouette and response tests

Assert named objects for rear-silhouette, rear-light-bar, rear-glass, rear-fascia, and both visible rear wheels. Project bounds through desktop and mobile chase cameras and assert the car remains inside the lower viewport. Assert that acceleration changes suspension, light intensity, and underglow without changing child count.

  • Step 2: Run car tests and verify failure

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts

Expected: FAIL because the current box car lacks the new named structure and detail mode.

  • Step 3: Build the faceted body and readable rear silhouette

Construct the body from original extruded side and rear profiles, not a scaled box. Use a low roof, wide haunches, tapered rear deck, recessed dark fascia, full-width rear light bar, dark glass canopy, visible wheel arches, and restrained cyan ground light. Keep all materials original and resource-owned.

  • Step 4: Add damped motion and responsive detail

Use internal spring state for rear squat and settle. Drive cruise vibration from a low-amplitude road frequency, keep steering sway below 0.006 radians, and hide secondary trim in reduced detail while retaining silhouette, glass, light bar, wheels, and underglow.

  • Step 5: Verify car bounds and allocation stability

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts && npm run typecheck

Expected: PASS.

  • Step 6: Commit the car
git add src/scene/world/car.ts src/scene/world/environment.ts tests/unit/world-models.test.ts tests/unit/scene-resources.test.ts
git commit -m "feat: reconstruct the chase-view grand tourer"

Task 6: Rebuild the environmental route sign and integrate the scene

Files:

  • Modify: src/scene/world/sign.ts
  • Modify: src/scene/world/environment.ts
  • Modify: src/scene/types.ts
  • Modify: tests/unit/world-models.test.ts
  • Modify: tests/unit/scene-lifecycle.test.ts
  • Modify: tests/browser/journey-integration.spec.ts

Interfaces:

  • Consumes: SignSnapshot, canonical navigation labels, current quality, and viewport aspect.

  • Produces: one roadside sign that updates its text texture only when the latched target changes.

  • Step 1: Write sign-object and scene-frame assertions

Assert plain labels such as Infrastructure and Selected builds, no route numbers, one texture update per target, no mesh allocation during lifecycle updates, and invisible idle/cooldown rendering.

  • Step 2: Run focused tests and verify failure

Run: npx vitest run tests/unit/world-models.test.ts tests/unit/scene-lifecycle.test.ts

Expected: FAIL because the existing sign uses numbered labels and geometry-mapped phases.

  • Step 3: Build a physical roadside sign

Use a frame, backplate, two mounting posts, a dark 12px-equivalent face, cyan edge light, and canvas text in Geist Mono. Position it on the right shoulder for desktop and closer to the horizon on narrow screens. Map anticipate, approach, dwell, and depart to transform and opacity only.

  • Step 4: Integrate all world modules in one scene frame

Keep environment.ts orchestration-only. Compute deltaSeconds once, update drive distance once, pass the same snapshot to road, terrain, sun, atmosphere, car, sign, palette, camera FOV, and post-processing. Smooth FOV between 52 and 59 degrees based on acceleration.

  • Step 5: Verify lifecycle, journey integration, and type safety

Run:

npx vitest run tests/unit/world-models.test.ts tests/unit/scene-lifecycle.test.ts
npx playwright test tests/browser/journey-integration.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS.

  • Step 6: Commit the integrated scene milestone
git add src/scene tests/unit/world-models.test.ts tests/unit/scene-lifecycle.test.ts tests/browser/journey-integration.spec.ts
git commit -m "feat: integrate the cinematic nightdrive scene"

Task 7: Replace exposed controls with the compact vehicle HUD

Files:

  • Create: src/components/IconGlyph.vue
  • Modify: src/components/DriveControls.vue
  • Modify: src/components/JourneyHud.vue
  • Modify: src/components/AppShell.vue
  • Modify: src/styles/world.css
  • Modify: tests/unit/SceneCanvas.test.ts
  • Modify: tests/browser/audio-controls.spec.ts
  • Modify: tests/browser/scene-controls.spec.ts
  • Modify: tests/browser/responsive-layout.spec.ts

Interfaces:

  • Consumes: existing motion policy, quality setter, scene state, audio state, mute state, volume state, and journey snapshot.

  • Produces: pre-activation START THE DRIVE, collapsed running controls, settings popover, and compact route HUD.

  • Step 1: Write UI-state and accessibility tests

Assert:

expect(wrapper.get('[data-drive-state="idle"]').text()).toContain('START THE DRIVE');
expect(wrapper.find('[aria-label="Mute sound"]').exists()).toBe(false);
await wrapper.get('[aria-label="Start the drive"]').trigger('click');
expect(wrapper.emitted('audioStart')).toHaveLength(1);

For running state, require accessible names for mute, volume, pause, and settings; a settings popover containing quality; 44px targets; Escape dismissal; click-outside dismissal; and visible focus. Journey HUD must contain only route percent, current section, and arrival state.

  • Step 2: Run component and browser tests and verify failure

Run: npx vitest run tests/unit/SceneCanvas.test.ts && npx playwright test tests/browser/audio-controls.spec.ts tests/browser/scene-controls.spec.ts --project=desktop-chromium

Expected: FAIL because the current controls expose every setting and use text-only buttons.

  • Step 3: Implement the icon contract and control states

Use tree-shaken Phosphor imports with a standard 20px, weight regular, and visual stroke weight. Keep button labels in aria-label and tooltips, not duplicate visible strings.

<button class="drive-start" type="button" aria-label="Start the drive" @click="emit('audioStart')">
  <IconGlyph name="play" aria-hidden="true" />
  <span>START THE DRIVE</span>
</button>

Collapse to mute, volume access, pause/resume, and settings after activation. Put quality and secondary status inside the upward-opening settings popover.

  • Step 4: Implement compact, non-overlapping HUD geometry

Desktop: controls lower left, route lower right, car center clear. Mobile: one safe-area-aware lower row with the settings popover opening upward. Short landscape: render controls in normal flow near Contact. Use only transform and opacity for transitions.

  • Step 5: Verify controls across state and viewport matrices

Run:

npx vitest run tests/unit/SceneCanvas.test.ts
npx playwright test tests/browser/audio-controls.spec.ts tests/browser/scene-controls.spec.ts tests/browser/responsive-layout.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS.

  • Step 6: Commit the HUD milestone
git add src/components/IconGlyph.vue src/components/DriveControls.vue src/components/JourneyHud.vue src/components/AppShell.vue src/styles/world.css tests/unit/SceneCanvas.test.ts tests/browser/audio-controls.spec.ts tests/browser/scene-controls.spec.ts tests/browser/responsive-layout.spec.ts
git commit -m "feat: introduce the compact vehicle HUD"
  • Step 7: Run the single approved milestone review

Request one focused review covering scene composition, car readability, sign lifecycle, HUD overlap, resource ownership, and regressions. Apply only high-confidence corrections, rerun the focused tests, and do not request per-task reviews.

Task 8: Add the original procedural synthwave soundtrack

Files:

  • Modify: src/audio/types.ts
  • Create: src/audio/music-sequencer.ts
  • Modify: src/audio/synth-engine.ts
  • Modify: src/audio/create-audio-controller.ts
  • Modify: src/composables/useAudioEngine.ts
  • Modify: tests/unit/audio-lifecycle.test.ts
  • Modify: tests/browser/audio-controls.spec.ts
  • Modify: tests/browser/audio-interruption.spec.ts

Interfaces:

  • Consumes: existing explicit activation and lifecycle controller plus smoothed drive intensity in [0, 1].
  • Produces: independently balanced music, engine, and road buses routed through one compressor and master gain.
export interface MusicSequencer {
  start(atTime: number): void;
  scheduleThrough(horizonTime: number): void;
  setIntensity(value: number, atTime: number): void;
  stop(): void;
}
  • Step 1: Extend fake-audio tests for the musical graph

Add fake context support for scheduled oscillators, envelopes, and look-ahead timers. Assert no graph before gesture, one graph after repeated activation, three independently routed buses, a compressor before output, a fixed 86 BPM clock, a four-chord minor progression, look-ahead scheduling, intensity-linked filter/percussion changes, and exact stop/disconnect/close behavior.

  • Step 2: Run audio tests and verify failure

Run: npx vitest run tests/unit/audio-lifecycle.test.ts

Expected: FAIL because no music sequencer or music bus exists.

  • Step 3: Implement the fixed musical arrangement

Use a 86 BPM four-bar loop with the progression F minor -> D-flat major -> A-flat major -> E-flat major. Schedule slow pad triads, root-and-fifth bass, sparse kick, soft noise clap, closed pulse percussion, and one infrequent two-note motif. Use Web Audio look-ahead scheduling with a 100ms scheduler interval and 250ms horizon.

  • Step 4: Route and couple buses without destabilizing harmony

Use this exact graph:

music voices -> music filter -> music gain \
engine voices -> engine filter -> engine gain -> compressor -> master gain -> output
road noise -> road filter -> road gain       /

Intensity raises music cutoff, pulse-percussion level, engine frequency, and road texture. It does not change BPM, chord order, or melody order.

  • Step 5: Preserve lifecycle and verify browser recovery

Run:

npx vitest run tests/unit/audio-lifecycle.test.ts
npx playwright test tests/browser/audio-controls.spec.ts tests/browser/audio-interruption.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS with no audio before gesture and deterministic silence on hidden, interruption, failure, and dispose paths.

  • Step 6: Commit the soundtrack
git add src/audio src/composables/useAudioEngine.ts tests/unit/audio-lifecycle.test.ts tests/browser/audio-controls.spec.ts tests/browser/audio-interruption.spec.ts
git commit -m "feat: add the procedural nightdrive soundtrack"

Task 9: Recompose the semantic information layer

Files:

  • Create: src/components/HeroJourney.vue
  • Modify: src/components/AppShell.vue
  • Modify: src/components/PortfolioSection.vue
  • Modify: src/components/SiteNavigation.vue
  • Modify: src/components/ExperienceTimeline.vue
  • Modify: src/components/ProjectCaseStudy.vue
  • Modify: src/components/ProjectCard.vue
  • Modify: src/components/ContactDestination.vue
  • Modify: src/styles/base.css
  • Modify: src/styles/layout.css
  • Modify: src/styles/components.css
  • Modify: src/styles/print.css
  • Modify: content/public/portfolio-content.json
  • Modify: tests/fixtures/portfolio-content.valid.json
  • Modify: tests/unit/AppShell.test.ts
  • Modify: tests/unit/components.test.ts
  • Modify: tests/browser/responsive-layout.spec.ts
  • Modify: tests/browser/semantic.spec.ts

Interfaces:

  • Consumes: validated portfolio content, canonical navigation, native anchors, project visibility, and automatic-video policy.

  • Produces: eight semantically identical sections with distinct layout families and clear project priority.

  • Step 1: Rewrite semantic and hierarchy tests

Require one h1 in the Start section, seven following h2 headings in sentence case, zero route-number labels, Explore work linking to #selected-builds, and Start a conversation linking to Luca's email. Assert the first two selected projects receive data-project-priority="primary" and Opinion Dynamics receives data-project-priority="supporting".

  • Step 2: Run focused tests and verify failure

Run: npx vitest run tests/unit/AppShell.test.ts tests/unit/components.test.ts

Expected: FAIL because the current document uses numbered uppercase section shells and equal project treatment.

  • Step 3: Implement the first-screen hero and compact navigation

The hero contains exactly these content units: Luca's name, the equal role System Administrator & Full-Stack Developer, the value proposition, and an Explore work action. Keep START THE DRIVE in the HUD, visible in the first viewport. Move the desktop navigation into one line at no more than 72px tall; use a compact accessible menu below 768px.

  • Step 4: Assign explicit layout families

Use data-layout values rather than nth-child alternation:

const sectionLayouts: Readonly<Record<SectionId, SectionLayout>> = {
  start: 'hero',
  profile: 'right-rail',
  experience: 'left-timeline',
  infrastructure: 'full-diagram',
  'selected-builds': 'project-stage',
  'additional-work': 'compact-rail',
  toolkit: 'toolkit-field',
  contact: 'destination',
};

Use one eyebrow at most in every three sections. Do not recreate the alternating half-panel pattern.

  • Step 5: Implement project and destination hierarchy

Render the medication tracker and Footy Tracker as large evidence-led case studies. Render Opinion Dynamics as a smaller research panel. Render Additional work as compact, variable-width summaries. Add the exact destination copy Thanks for making the trip. The road's open if you'd like to build something together. and a Start a conversation mail action.

  • Step 6: Build responsive and print layout rules

At 1280px and wider, maintain an open center corridor. At 768px to 1279px, offset panels while protecting the car. Below 768px, use one readable column with --surface-strong; no text may sit directly over bright scenery. Preserve the complete, clean print document and native focus order.

  • Step 7: Verify semantics and responsive geometry

Run:

npx vitest run tests/unit/AppShell.test.ts tests/unit/components.test.ts
npx playwright test tests/browser/semantic.spec.ts tests/browser/responsive-layout.spec.ts tests/browser/no-javascript.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS.

  • Step 8: Commit the information layer
git add src/components src/styles content/public/portfolio-content.json tests/fixtures/portfolio-content.valid.json tests/unit/AppShell.test.ts tests/unit/components.test.ts tests/browser/responsive-layout.spec.ts tests/browser/semantic.spec.ts
git commit -m "feat: recompose the portfolio information layer"

Task 10: Build the infrastructure flow and official icon registries

Files:

  • Create: src/components/BrandIcon.vue
  • Create: src/icons/brand-icons.ts
  • Modify: src/components/InfrastructureDiagram.vue
  • Modify: src/components/ToolkitGrid.vue
  • Modify: src/components/ContactDestination.vue
  • Modify: src/styles/components.css
  • Create: tests/unit/brand-icons.test.ts
  • Modify: tests/unit/components.test.ts
  • Modify: tests/browser/accessibility-axe.spec.ts
  • Modify: tests/browser/responsive-layout.spec.ts

Interfaces:

  • Consumes: toolkit technology labels, social service labels, Phosphor interface icons, Simple Icons brand data, and infrastructure evidence bands.
  • Produces: exhaustive brand-icon lookup and a responsive traffic-flow diagram with a visible text equivalent.
export interface BrandIconDefinition {
  title: string;
  hex: string;
  path: string;
}

export function brandIconFor(label: string): BrandIconDefinition;
  • Step 1: Write exhaustive registry and diagram tests

Read the validated fixture and assert every toolkit technology and social service resolves to an icon. Require the diagram to expose nodes for Visitor, Nginx and TLS, Authentication, Containerized services, and Operations, plus accessible trust-boundary labels and the existing visible text equivalent.

  • Step 2: Run icon and component tests and verify failure

Run: npx vitest run tests/unit/brand-icons.test.ts tests/unit/components.test.ts

Expected: FAIL because the registry and real flow diagram do not exist.

  • Step 3: Implement brand and interface icon rendering

Import named Simple Icons exports only in brand-icons.ts. Render package-provided paths through BrandIcon.vue; hide decorative marks from assistive technology and keep the visible technology or service name. Continue using Phosphor only for interface and diagram actions.

  • Step 4: Replace the four-box diagram with a flow

Desktop flow: Visitor -> Nginx and TLS -> Authentication -> Containerized services, with public, access, and service trust boundaries. Place fail2ban at ingress and lifecycle/maintenance below the services as operational support. Mobile: convert to a vertical flow with full labels and no clipped connectors.

  • Step 5: Verify accessibility and responsive diagram behavior

Run:

npx vitest run tests/unit/brand-icons.test.ts tests/unit/components.test.ts
npx playwright test tests/browser/accessibility-axe.spec.ts tests/browser/responsive-layout.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS.

  • Step 6: Commit diagrams and iconography
git add src/components/BrandIcon.vue src/icons/brand-icons.ts src/components/InfrastructureDiagram.vue src/components/ToolkitGrid.vue src/components/ContactDestination.vue src/styles/components.css tests/unit/brand-icons.test.ts tests/unit/components.test.ts tests/browser/accessibility-axe.spec.ts tests/browser/responsive-layout.spec.ts
git commit -m "feat: add infrastructure flow and official iconography"

Files:

  • Modify: content/public/portfolio-content.json
  • Modify: content/public/asset-manifest.json
  • Modify: content/public/asset-recipes.json
  • Modify: tests/fixtures/portfolio-content.valid.json
  • Modify: tests/fixtures/asset-manifest.valid.json
  • Modify: src/components/ProjectMedia.vue
  • Modify: src/styles/components.css
  • Modify: tests/integration/prepare-assets.test.ts
  • Modify: tests/integration/public-content.test.ts
  • Modify: tests/unit/ProjectMedia.test.ts
  • Modify: tests/browser/media-policy.spec.ts
  • Modify: tests/browser/media-failure.spec.ts

Interfaces:

  • Consumes: verified repository URL map, Luca-approved original source files, existing asset sanitizer, exact asset closure schema, and media policy.

  • Produces: public links only for confidently matched public projects and privacy-clean derivatives referenced by exact manifest IDs.

  • Step 1: Add verified-link and provenance tests

Assert each added URL uses HTTPS, contains no query or fragment, points to the verified owner profile, and appears only on a public project. Assert each media presentation has an exact manifest closure, nonempty alt text, caption, dimensions, checksum, rights holder, license, and attribution.

  • Step 2: Run content and media tests and verify failure

Run: npx vitest run tests/integration/public-content.test.ts tests/integration/prepare-assets.test.ts tests/unit/ProjectMedia.test.ts

Expected: FAIL for the new verified links and, after Luca supplies media, for the unprepared source assets.

  • Step 3: Add the confidently verified repository links

Apply this exact map and leave private projects unlinked:

const verifiedProjectUrls = {
  'footy-tracker': 'https://github.com/LucaPisl/footy-tracker',
  'opinion-dynamics-simulation': 'https://github.com/LucaPisl/opinion-dynamics-simulation',
  'currency-converter': 'https://github.com/LucaPisl/CurrencyConverterAvalonia',
  'linux-modding-guide': 'https://github.com/LucaPisl/LinuxModdingGuide',
} as const;

Do not use the stale CurrencyConverterAvaloniaProject clone path found inside the Currency Converter README; it returns 404 and is not the repository URL.

  • Step 4: Stop at the authentic-media input gate when no sources exist

Inspect inputs/source-assets/ for Luca-approved binaries. If it is empty, keep all media and assetIds fields empty, do not invent images, and record the final acceptance item as blocked by missing source media. Continue all other tasks.

  • Step 5: When approved sources exist, prepare and bind derivatives

Run npm run prepare:assets, inspect every derivative for private identifiers, update recipes and manifest entries, and bind the exact IDs to their owning project. Use still images before video; video remains click-to-play unless the existing automatic-video policy allows it.

  • Step 6: Verify public content and media failure states

Run:

npm run verify:assets
npx vitest run tests/integration/public-content.test.ts tests/integration/prepare-assets.test.ts tests/unit/ProjectMedia.test.ts
npx playwright test tests/browser/media-policy.spec.ts tests/browser/media-failure.spec.ts --project=desktop-chromium
npm run typecheck

Expected: PASS for the available asset set. If no sources are available, the site passes with no project-media elements and the final visual acceptance remains explicitly incomplete.

  • Step 7: Commit links and any approved media
git add content tests/fixtures src/components/ProjectMedia.vue src/styles/components.css tests/integration/public-content.test.ts tests/integration/prepare-assets.test.ts tests/unit/ProjectMedia.test.ts tests/browser/media-policy.spec.ts tests/browser/media-failure.spec.ts
git commit -m "feat: integrate verified project evidence"

Task 12: Perform integrated browser refinement and final verification

Files:

  • Modify: src/styles/layout.css
  • Modify: src/styles/components.css
  • Modify: src/styles/world.css
  • Modify: src/scene/world/*.ts only for evidence-backed visual calibration
  • Modify: tests/browser/visual-regression.spec.ts
  • Modify: tests/browser/responsive-layout.spec.ts
  • Modify: docs/reviews/visual-qa.md

Interfaces:

  • Consumes: the integrated preview, reference site, representative viewport matrix, motion policies, failure injection, and all test suites.

  • Produces: a working local preview and documented visual evidence for Luca's final review.

  • Step 1: Build the production-like preview

Run: npm run build:preview

Expected: PASS with a complete dist artifact.

  • Step 2: Inspect the reference and portfolio side by side

Use the in-app browser at these exact viewports: 320x568, 390x844, 844x390, 768x1024, 1280x720, 1440x900, 1920x1080, and 2560x1440. Compare depth, road convergence, terrain density, horizon balance, bloom restraint, speed response, and lower-dashboard footprint. Do not copy reference branding or content.

  • Step 3: Iterate one integrated composition at a time

At each viewport verify navigation, hero, car, sign, bright-scene text contrast, HUD overlap, project hierarchy, diagram legibility, toolkit marks, destination, and safe areas. Calibrate existing variables and transforms; do not introduce a second visual system.

  • Step 4: Verify motion and resilient states

Inspect normal motion, high acceleration, stopped scroll during sign dwell, reverse scroll, explicit anchor jump, reduced motion, Static mode, no JavaScript, WebGL failure, first-frame timeout, context loss, audio failure, and print.

  • Step 5: Run the complete verification matrix

Run:

npm run typecheck
npm run test:unit
npm run build:preview
npm run test:browser
npm run test:deployment
npm run check:budgets
git diff --check

Expected: every applicable check passes. Do not weaken existing budgets or assertions merely to obtain a pass.

  • Step 6: Run the one final integrated review

Request one final review covering correctness, accessibility, responsive composition, scene/resource lifecycle, audio lifecycle, content privacy, and user-facing polish. Apply high-confidence fixes and rerun the affected checks plus the complete unit suite.

  • Step 7: Commit final refinements
git add src tests docs/reviews/visual-qa.md
git commit -m "fix: refine the integrated portfolio experience"
  • Step 8: Present the working preview for Luca's visual approval

Open the preview in Codex and report the exact media-input status. Do not describe the redesign as finished until Luca has inspected the working preview and approved it.