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

971 lines
45 KiB
Markdown

# 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:
```ts
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:
```bash
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`:
```ts
import '@fontsource-variable/geist/wght.css';
import '@fontsource-variable/geist-mono/wght.css';
```
Define the stable visual tokens:
```css
: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**
```bash
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`:
```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:
```ts
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:
```ts
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:
```ts
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`:
```ts
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:
```bash
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**
```bash
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.
```ts
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:
```ts
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.
```ts
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**
```bash
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.
```ts
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:
```bash
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**
```bash
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.
```ts
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**
```bash
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:
```bash
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**
```bash
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:
```ts
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.
```vue
<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:
```bash
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**
```bash
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.
```ts
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:
```text
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:
```bash
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**
```bash
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:
```ts
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:
```bash
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**
```bash
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.
```ts
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:
```bash
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**
```bash
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"
```
### Task 11: Integrate verified public links and authentic media
**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:
```ts
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:
```bash
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**
```bash
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:
```bash
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**
```bash
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.