# SilverMetal Windows — First-Boot Experience & Branding 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:** Bake SilverMetal Windows' four branding layers declaratively into the WIM, lock the one-time onboarding session into a hardened kiosk, and present the Welcome app as a fullscreen branded glass card.

**Architecture:** A shared dual-mode PowerShell branding module (`windows/branding/`) writes registry + stages assets, run offline by `build.ps1`'s `Invoke-Brand` and online by self-apply. A build-only kiosk uses Shell Launcher v2 + Keyboard Filter configured at end-of-setup for the `sm-bootstrap` session. The MAUI Welcome app becomes a borderless fullscreen window with a CSS-only frosted-glass presentation.

**Tech Stack:** PowerShell 5.1 + DISM + offline registry (`reg load`), Pester v5 tests, Windows IoT Enterprise LTSC features (Shell Launcher v2 / Keyboard Filter via WMI), .NET MAUI Blazor (WebView2) + WinUI AppWindow, CSS.

**Spec:** [`../specs/2026-06-09-first-boot-branding-design.md`](../specs/2026-06-09-first-boot-branding-design.md)

**Branch:** `feat/first-boot-branding` (already created off `origin/main`; spec committed at `66e7fd4`).

**Conventions to follow (from the existing repo):**
- `build.ps1` already does offline `reg load`/`reg unload` with a `[gc]::Collect(); Start-Sleep` guard before unload — reuse that idiom.
- Hardening modules are `windows/hardening/NN-*.ps1`, `Set-StrictMode -Version Latest`, `$ErrorActionPreference='Stop'`, `Write-Stage`/`Write-Host` logging. Match that style.
- Tests live in `windows/tests/`. The repo already has `Assert-IsoStructure.ps1`.

---

## Phase A — Branding module (`windows/branding/`)

Self-contained and testable on any Windows box with Pester — **no ISO, no hardware**. Do this phase first.

### Task A1: Branding manifest + asset folder skeleton

**Files:**
- Create: `windows/branding/branding.manifest.json`
- Create: `windows/branding/assets/README.md`
- Create: `windows/branding/README.md`

- [ ] **Step 1: Write the manifest (single source of truth for all branding strings)**

`windows/branding/branding.manifest.json`:
```json
{
  "schemaVersion": 1,
  "productName": "SilverMetal Windows",
  "oem": {
    "manufacturer": "SilverLABS",
    "model": "SilverMetal Windows",
    "supportUrl": "https://silverlabs.uk",
    "supportHours": "24/7 community + paid SLA",
    "logo": "oemlogo.bmp"
  },
  "bitlocker": {
    "recoveryMessage": "SilverMetal Windows. Locked out? silverlabs.uk",
    "recoveryUrl": "https://silverlabs.uk"
  },
  "lockScreen": {
    "image": "lockscreen.jpg",
    "lock": true
  },
  "desktop": {
    "wallpaper": "wallpaper.jpg",
    "theme": "SilverMetal.theme",
    "accentBgr": "00d4ff",
    "darkMode": true,
    "lockWallpaper": false
  }
}
```

- [ ] **Step 2: Write the assets README (documents required placeholder assets)**

`windows/branding/assets/README.md`:
```markdown
# Branding assets

Placeholder void/cyan assets until the brand identity is finalised
(`shared/branding/README.md`). Replace in place; keep filenames.

| File | Spec | Used by |
|------|------|---------|
| `oemlogo.bmp` | 120×120 24-bit BMP | OEM About logo |
| `lockscreen.jpg` | display-resolution JPG, dark | Lock/sign-in |
| `wallpaper.jpg` | display-resolution JPG, dark | Desktop |
| `SilverMetal.theme` | Windows .theme (dark + cyan accent) | Desktop theme |
```

- [ ] **Step 3: Write the module README**

`windows/branding/README.md`:
```markdown
# SilverMetal Windows branding (shared, dual-mode)

`Apply-Branding.ps1` writes the four branding layers either OFFLINE into a
mounted WIM (`-Mode Offline -MountPath <mount>`, used by `installer/build.ps1`
`Invoke-Brand`) or ONLINE onto the running system (`-Mode Online`, self-apply).

Strings live in `branding.manifest.json`; images in `assets/`. See
`docs/superpowers/specs/2026-06-09-first-boot-branding-design.md`.

Layers: BitLocker pre-boot recovery message, lock-screen image (locked),
desktop wallpaper+theme (changeable), OEM About.
```

- [ ] **Step 4: Commit**

```bash
git add windows/branding
git commit -m "feat(branding): manifest + module skeleton for SilverMetal Windows branding"
```

---

### Task A2: Registry hive abstraction + Pester harness

The module must write to a hive root that is either a loaded offline hive (`HKLM\SM_*`) or the live hive (`HKLM:`/default-user). Build a tiny helper that takes a **PSDrive-style root** so tests can point it at a throwaway key.

**Files:**
- Create: `windows/branding/lib/RegistryHelpers.ps1`
- Test: `windows/tests/Branding.Tests.ps1`

- [ ] **Step 1: Write the failing test**

`windows/tests/Branding.Tests.ps1`:
```powershell
#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' }
. "$PSScriptRoot\..\branding\lib\RegistryHelpers.ps1"

Describe 'Set-SmRegValue' {
    BeforeAll {
        $script:root = 'HKCU:\Software\SilverMetalTest'
        if (Test-Path $root) { Remove-Item $root -Recurse -Force }
    }
    AfterAll {
        if (Test-Path $script:root) { Remove-Item $script:root -Recurse -Force }
    }

    It 'creates the key path and writes a string value' {
        Set-SmRegValue -Root $script:root -SubKey 'A\B' -Name 'Greeting' -Type String -Value 'hi'
        (Get-ItemProperty "$script:root\A\B").Greeting | Should -Be 'hi'
    }

    It 'writes a dword value' {
        Set-SmRegValue -Root $script:root -SubKey 'A' -Name 'Flag' -Type DWord -Value 1
        (Get-ItemProperty "$script:root\A").Flag | Should -Be 1
    }
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`
Expected: FAIL — `RegistryHelpers.ps1` not found / `Set-SmRegValue` not defined.

- [ ] **Step 3: Implement the helper**

`windows/branding/lib/RegistryHelpers.ps1`:
```powershell
Set-StrictMode -Version Latest

# Write a registry value under an arbitrary hive root (a live HKLM:/HKCU: path
# OR a loaded offline hive exposed as a PSDrive). Creates intermediate keys.
function Set-SmRegValue {
    param(
        [Parameter(Mandatory)][string]$Root,
        [Parameter(Mandatory)][string]$SubKey,
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][ValidateSet('String','ExpandString','DWord','Binary')][string]$Type,
        [Parameter(Mandatory)]$Value
    )
    $key = Join-Path $Root $SubKey
    if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null }
    New-ItemProperty -Path $key -Name $Name -PropertyType $Type -Value $Value -Force | Out-Null
}
```

- [ ] **Step 4: Run it to verify it passes**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`
Expected: PASS (2 tests).

- [ ] **Step 5: Commit**

```bash
git add windows/branding/lib windows/tests/Branding.Tests.ps1
git commit -m "feat(branding): registry helper + Pester harness"
```

---

### Task A3: Layer writers (pure functions over a hive root)

Each layer is a function that writes its values under a given `SOFTWARE` root and/or default-user root. Pure registry writes — testable against throwaway keys. Asset staging is separate (Task A5).

**Files:**
- Create: `windows/branding/lib/BrandingLayers.ps1`
- Modify: `windows/tests/Branding.Tests.ps1` (append)

- [ ] **Step 1: Write the failing tests (append to `Branding.Tests.ps1`)**

```powershell
. "$PSScriptRoot\..\branding\lib\BrandingLayers.ps1"

Describe 'Branding layer writers' {
    BeforeAll {
        $script:sw = 'HKCU:\Software\SilverMetalTest\SW'
        $script:du = 'HKCU:\Software\SilverMetalTest\DU'
        $script:m  = Get-Content "$PSScriptRoot\..\branding\branding.manifest.json" -Raw | ConvertFrom-Json
    }
    AfterAll {
        Remove-Item 'HKCU:\Software\SilverMetalTest' -Recurse -Force -ErrorAction SilentlyContinue
    }

    It 'writes OEM About info' {
        Set-OemInformation -SoftwareRoot $script:sw -Manifest $script:m -LogoPath 'C:\Windows\System32\oemlogo.bmp'
        $k = Get-ItemProperty "$script:sw\Microsoft\Windows\CurrentVersion\OEMInformation"
        $k.Manufacturer | Should -Be 'SilverLABS'
        $k.Model        | Should -Be 'SilverMetal Windows'
        $k.SupportURL   | Should -Be 'https://silverlabs.uk'
        $k.Logo         | Should -Be 'C:\Windows\System32\oemlogo.bmp'
    }

    It 'writes a locked lock-screen image' {
        Set-LockScreen -SoftwareRoot $script:sw -ImagePath 'C:\Windows\Web\Screen\SilverMetal\lockscreen.jpg' -Lock $true
        (Get-ItemProperty "$script:sw\Microsoft\Windows\CurrentVersion\PersonalizationCSP").LockScreenImageStatus | Should -Be 1
        (Get-ItemProperty "$script:sw\Policies\Microsoft\Windows\Personalization").NoChangingLockScreen | Should -Be 1
    }

    It 'writes desktop wallpaper + dark mode into the default-user root' {
        Set-DesktopBranding -DefaultUserRoot $script:du -Manifest $script:m -WallpaperPath 'C:\Windows\Web\Wallpaper\SilverMetal\wallpaper.jpg'
        (Get-ItemProperty "$script:du\Control Panel\Desktop").WallPaper | Should -Be 'C:\Windows\Web\Wallpaper\SilverMetal\wallpaper.jpg'
        (Get-ItemProperty "$script:du\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize").AppsUseLightTheme | Should -Be 0
    }

    It 'writes the BitLocker pre-boot recovery message policy' {
        Set-BitLockerPreboot -SoftwareRoot $script:sw -Manifest $script:m
        $k = Get-ItemProperty "$script:sw\Policies\Microsoft\FVE"
        $k.UseCustomRecoveryMessage | Should -Be 1
        $k.RecoveryMessage          | Should -Be 'SilverMetal Windows. Locked out? silverlabs.uk'
    }
}
```

- [ ] **Step 2: Run to verify failure**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`
Expected: FAIL — layer functions not defined.

- [ ] **Step 3: Implement the layer writers**

`windows/branding/lib/BrandingLayers.ps1`:
```powershell
Set-StrictMode -Version Latest
. "$PSScriptRoot\RegistryHelpers.ps1"

function Set-OemInformation {
    param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)][string]$LogoPath)
    $sub = 'Microsoft\Windows\CurrentVersion\OEMInformation'
    Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Manufacturer' -Type String -Value $Manifest.oem.manufacturer
    Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Model'        -Type String -Value $Manifest.oem.model
    Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'SupportURL'   -Type String -Value $Manifest.oem.supportUrl
    Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'SupportHours' -Type String -Value $Manifest.oem.supportHours
    Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Logo'         -Type String -Value $LogoPath
}

function Set-LockScreen {
    param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)][string]$ImagePath,[bool]$Lock=$true)
    # Per-device modern lock-screen image (reliable on Enterprise/IoT).
    $csp = 'Microsoft\Windows\CurrentVersion\PersonalizationCSP'
    Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImagePath'   -Type String -Value $ImagePath
    Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImageUrl'    -Type String -Value $ImagePath
    Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImageStatus' -Type DWord  -Value 1
    if ($Lock) {
        $pol = 'Policies\Microsoft\Windows\Personalization'
        Set-SmRegValue -Root $SoftwareRoot -SubKey $pol -Name 'LockScreenImage'     -Type String -Value $ImagePath
        Set-SmRegValue -Root $SoftwareRoot -SubKey $pol -Name 'NoChangingLockScreen' -Type DWord  -Value 1
    }
}

function Set-DesktopBranding {
    param([Parameter(Mandatory)][string]$DefaultUserRoot,[Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)][string]$WallpaperPath)
    Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Control Panel\Desktop' -Name 'WallPaper'      -Type String -Value $WallpaperPath
    Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Control Panel\Desktop' -Name 'WallpaperStyle' -Type String -Value '10'   # fill
    if ($Manifest.desktop.darkMode) {
        $p = 'Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
        Set-SmRegValue -Root $DefaultUserRoot -SubKey $p -Name 'AppsUseLightTheme'   -Type DWord -Value 0
        Set-SmRegValue -Root $DefaultUserRoot -SubKey $p -Name 'SystemUsesLightTheme' -Type DWord -Value 0
    }
    # Accent (cyan). BGR DWORD from manifest hex (stored little-endian as 0x00BBGGRR).
    $bgr = [Convert]::ToInt32($Manifest.desktop.accentBgr,16)
    Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\DWM' -Name 'AccentColor'       -Type DWord -Value $bgr
    Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\DWM' -Name 'ColorizationColor' -Type DWord -Value $bgr
    if (-not $Manifest.desktop.lockWallpaper) { return }
    Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop' -Name 'NoChangingWallPaper' -Type DWord -Value 1
}

function Set-BitLockerPreboot {
    param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)]$Manifest)
    # GPO "Configure pre-boot recovery message and URL" (ADMX VolumeEncryption).
    # NOTE: only the BitLocker RECOVERY screen is customisable; the normal PIN-entry
    # screen text is fixed Windows UI. Exact value names are asserted by the read-back
    # test; if a name is wrong the offline-apply verify (Task A4) catches it.
    $fve = 'Policies\Microsoft\FVE'
    Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'UseCustomRecoveryMessage' -Type DWord  -Value 1
    Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'RecoveryMessage'          -Type String -Value $Manifest.bitlocker.recoveryMessage
    Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'UseCustomRecoveryUrl'     -Type DWord  -Value 1
    Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'RecoveryUrl'              -Type String -Value $Manifest.bitlocker.recoveryUrl
}
```

- [ ] **Step 4: Run to verify pass**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`
Expected: PASS (all layer tests green).

- [ ] **Step 5: Commit**

```bash
git add windows/branding/lib/BrandingLayers.ps1 windows/tests/Branding.Tests.ps1
git commit -m "feat(branding): OEM/lockscreen/desktop/bitlocker layer writers + tests"
```

---

### Task A4: `Apply-Branding.ps1` orchestrator (offline + online hive mounting)

Ties the layers together and handles `reg load`/`reg unload` for offline mode and the default-user hive for online mode.

**Files:**
- Create: `windows/branding/Apply-Branding.ps1`
- Modify: `windows/tests/Branding.Tests.ps1` (append an offline integration test against a throwaway hive file)

- [ ] **Step 1: Write the failing offline-mode integration test (append)**

```powershell
Describe 'Apply-Branding -Mode Offline' {
    BeforeAll {
        # Build a throwaway "mount" tree with empty SOFTWARE + default NTUSER hives.
        $script:mount = Join-Path $env:TEMP ("sm-brandtest-" + [guid]::NewGuid().ToString('N'))
        New-Item -ItemType Directory -Force "$script:mount\Windows\System32\config","$script:mount\Users\Default" | Out-Null
        # Create two empty hive files by loading/unloading a fresh key.
        foreach ($h in @("$script:mount\Windows\System32\config\SOFTWARE","$script:mount\Users\Default\NTUSER.DAT")) {
            reg load 'HKLM\SM_SEED' (New-Item -ItemType File -Force $h | Select-Object -Expand FullName) 2>$null | Out-Null
        }
        # The above seed is best-effort; if reg can't init an empty file, the apply
        # script creates the hives via reg load of the path it expects.
    }
    AfterAll { Remove-Item $script:mount -Recurse -Force -ErrorAction SilentlyContinue }

    It 'applies all layers into the offline SOFTWARE hive and reports success' {
        $r = & "$PSScriptRoot\..\branding\Apply-Branding.ps1" -Mode Offline -MountPath $script:mount -PassThru
        $r.OemApplied        | Should -BeTrue
        $r.LockScreenApplied | Should -BeTrue
        $r.DesktopApplied    | Should -BeTrue
        $r.BitLockerApplied  | Should -BeTrue
    }
}
```

> Note: this test requires an **elevated** shell (`reg load` needs admin). The CI Windows runner runs elevated; document that in `windows/tests/README.md` (Task A6).

- [ ] **Step 2: Run to verify failure**

Run (elevated): `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1 -Tag ''"`
Expected: FAIL — `Apply-Branding.ps1` missing.

- [ ] **Step 3: Implement the orchestrator**

`windows/branding/Apply-Branding.ps1`:
```powershell
#Requires -Version 5.1
<#
.SYNOPSIS  Apply SilverMetal Windows branding (4 layers), offline (WIM) or online.
.DESCRIPTION
    Offline: reg-load the mounted image's SOFTWARE + default NTUSER hives, write
    values, stage assets, reg-unload. Online: write live HKLM + default-user hive.
    Design: ../docs/superpowers/specs/2026-06-09-first-boot-branding-design.md
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)][ValidateSet('Offline','Online')][string]$Mode,
    [string]$MountPath,                                   # required for Offline
    [string]$Manifest  = "$PSScriptRoot\branding.manifest.json",
    [string]$AssetsDir = "$PSScriptRoot\assets",
    [switch]$PassThru
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\lib\BrandingLayers.ps1"
function Write-Stage { param($m) Write-Host "==> $m" -ForegroundColor Cyan }

if ($Mode -eq 'Offline' -and -not $MountPath) { throw 'Offline mode requires -MountPath.' }
$m = Get-Content $Manifest -Raw | ConvertFrom-Json

# Destination paths (image-relative for offline, live for online).
$winRoot   = if ($Mode -eq 'Offline') { $MountPath } else { 'C:' }
$logoDest  = Join-Path $winRoot 'Windows\System32\oemlogo.bmp'
$lockDir   = Join-Path $winRoot 'Windows\Web\Screen\SilverMetal'
$wallDir   = Join-Path $winRoot 'Windows\Web\Wallpaper\SilverMetal'
$themeDest = Join-Path $winRoot 'Windows\Resources\Themes'
$lockLive  = 'C:\Windows\Web\Screen\SilverMetal\'   + $m.lockScreen.image
$wallLive  = 'C:\Windows\Web\Wallpaper\SilverMetal\' + $m.desktop.wallpaper
$logoLive  = 'C:\Windows\System32\oemlogo.bmp'

# --- stage assets ---
Write-Stage "Stage branding assets ($Mode)"
New-Item -ItemType Directory -Force $lockDir,$wallDir,$themeDest,(Split-Path $logoDest) | Out-Null
Copy-Item (Join-Path $AssetsDir $m.oem.logo)        $logoDest -Force
Copy-Item (Join-Path $AssetsDir $m.lockScreen.image) (Join-Path $lockDir $m.lockScreen.image) -Force
Copy-Item (Join-Path $AssetsDir $m.desktop.wallpaper)(Join-Path $wallDir $m.desktop.wallpaper) -Force
Copy-Item (Join-Path $AssetsDir $m.desktop.theme)    (Join-Path $themeDest $m.desktop.theme) -Force

$result = [ordered]@{ OemApplied=$false; LockScreenApplied=$false; DesktopApplied=$false; BitLockerApplied=$false }

function Invoke-WithHive {
    param([string]$HivePath,[string]$Name,[scriptblock]$Body)
    & reg load "HKLM\$Name" $HivePath | Out-Null
    if ($LASTEXITCODE -ne 0) { throw "reg load $Name ($HivePath) failed" }
    try { & $Body "Registry::HKEY_LOCAL_MACHINE\$Name" }
    finally { [gc]::Collect(); Start-Sleep -Milliseconds 500; & reg unload "HKLM\$Name" | Out-Null }
}

if ($Mode -eq 'Offline') {
    $swHive = Join-Path $MountPath 'Windows\System32\config\SOFTWARE'
    $duHive = Join-Path $MountPath 'Users\Default\NTUSER.DAT'
    Invoke-WithHive $swHive 'SM_BRAND_SW' {
        param($sw)
        Write-Stage 'OEM About';        Set-OemInformation  -SoftwareRoot $sw -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
        Write-Stage 'Lock screen';      Set-LockScreen      -SoftwareRoot $sw -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
        Write-Stage 'BitLocker preboot';Set-BitLockerPreboot -SoftwareRoot $sw -Manifest $m; $result.BitLockerApplied=$true
    }
    Invoke-WithHive $duHive 'SM_BRAND_DU' {
        param($du)
        Write-Stage 'Desktop'; Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
    }
} else {
    Set-OemInformation  -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
    Set-LockScreen      -SoftwareRoot 'HKLM:\SOFTWARE' -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
    Set-BitLockerPreboot -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m; $result.BitLockerApplied=$true
    Invoke-WithHive 'C:\Users\Default\NTUSER.DAT' 'SM_BRAND_DU' {
        param($du) Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
    }
}

Write-Host 'Branding applied.' -ForegroundColor Green
if ($PassThru) { [pscustomobject]$result }
```

- [ ] **Step 4: Provide placeholder assets so staging succeeds**

Create minimal placeholder files so `Copy-Item` works in tests/builds (real art lands later):
```powershell
# Generate tiny valid placeholders (run once, commit the outputs)
Add-Type -AssemblyName System.Drawing
$dir = 'windows/branding/assets'
$bmp = New-Object System.Drawing.Bitmap 120,120
$g = [System.Drawing.Graphics]::FromImage($bmp); $g.Clear([System.Drawing.Color]::FromArgb(11,15,20))
$bmp.Save("$dir/oemlogo.bmp", [System.Drawing.Imaging.ImageFormat]::Bmp)
$big = New-Object System.Drawing.Bitmap 1920,1200
$g2 = [System.Drawing.Graphics]::FromImage($big); $g2.Clear([System.Drawing.Color]::FromArgb(8,15,23))
$big.Save("$dir/lockscreen.jpg", [System.Drawing.Imaging.ImageFormat]::Jpeg)
$big.Save("$dir/wallpaper.jpg", [System.Drawing.Imaging.ImageFormat]::Jpeg)
@"
[Theme]
DisplayName=SilverMetal
[Control Panel\Desktop]
Wallpaper=%SystemRoot%\Web\Wallpaper\SilverMetal\wallpaper.jpg
WallpaperStyle=10
[VisualStyles]
SystemMode=Dark
AppMode=Dark
"@ | Set-Content "$dir/SilverMetal.theme" -Encoding ASCII
```

- [ ] **Step 5: Run the full branding test suite (elevated)**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`
Expected: PASS (helper + layers + offline integration).

- [ ] **Step 6: Commit**

```bash
git add windows/branding/Apply-Branding.ps1 windows/branding/assets windows/tests/Branding.Tests.ps1
git commit -m "feat(branding): Apply-Branding orchestrator (offline/online) + placeholder assets"
```

---

### Task A5: Wire `Invoke-Brand` in `build.ps1`

**Files:**
- Modify: `windows/installer/build.ps1:249-250` (replace the `Invoke-Brand` stub)

- [ ] **Step 1: Replace the stub**

Replace lines 249-250 (`function Invoke-Brand { ... deferred to M4. }`) with:
```powershell
# --- 5. Brand --------------------------------------------------------------
# NOTE: branding edits the OFFLINE hives, so it must run while the WIM is still
# mounted. We therefore call it from inside Invoke-ServiceWim (see Step 2), and
# this stage just asserts the staged result for the SBOM/log.
function Invoke-Brand {
    Write-Stage 'Stage 5: branding (applied during WIM servicing)'
    Write-Host '   branding layers baked via branding\Apply-Branding.ps1 -Mode Offline'
}
```

- [ ] **Step 2: Call branding inside the mounted-WIM block**

In `Invoke-ServiceWim`, after `Copy-WelcomePayload` (line 214) and before the UAC block, add:
```powershell
        # Bake the four branding layers into the offline hives (must be inside the mount).
        Write-Stage 'Stage 3e: bake SilverMetal branding (OEM/lockscreen/desktop/bitlocker)'
        & (Join-Path $WindowsDir 'branding\Apply-Branding.ps1') -Mode Offline -MountPath $mount
        if ($LASTEXITCODE -ne 0) { throw 'branding apply failed' }
```

- [ ] **Step 3: Lint-parse the script**

Run: `pwsh -NoProfile -Command "[void][System.Management.Automation.Language.Parser]::ParseFile('windows/installer/build.ps1',[ref]$null,[ref]$null); 'OK'"`
Expected: `OK`

- [ ] **Step 4: Commit**

```bash
git add windows/installer/build.ps1
git commit -m "feat(build): wire branding into Invoke-ServiceWim (offline hive bake)"
```

---

### Task A6: Test docs

**Files:**
- Modify: `windows/tests/README.md`

- [ ] **Step 1: Document the branding test + elevation requirement**

Append to `windows/tests/README.md`:
```markdown
## Branding.Tests.ps1

Pester v5 unit + offline-integration tests for `windows/branding/`.
**Requires an elevated shell** (the offline-integration test uses `reg load`).
Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"`.
```

- [ ] **Step 2: Commit**

```bash
git add windows/tests/README.md
git commit -m "docs(tests): document branding test suite + elevation requirement"
```

**Phase A done** → branding bakes offline and is unit-tested.

---

## Phase B — Hardened kiosk (build-only)

### Task B1: `Configure-Kiosk.ps1` — Shell Launcher v2 + Keyboard Filter

**Files:**
- Create: `windows/installer/oem/Configure-Kiosk.ps1`

- [ ] **Step 1: Implement the kiosk configurator (runs as SYSTEM at end-of-setup)**

`windows/installer/oem/Configure-Kiosk.ps1`:
```powershell
#Requires -Version 5.1
<#
.SYNOPSIS  Configure the one-time sm-bootstrap onboarding kiosk.
.DESCRIPTION
    Runs from SetupComplete.cmd as SYSTEM, after accounts exist, before first
    logon. Sets the sm-bootstrap shell to an elevating launcher for the Welcome
    app (no Explorer => no taskbar/Start), turns on the Keyboard Filter for shell
    hotkeys, and disables Task Manager / lock / fast-user-switch escapes.
    Reverted by the Welcome app's ApplyService on wizard success.
#>
[CmdletBinding()]
param([string]$BootstrapUser='sm-bootstrap',
      [string]$WelcomeExe='C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe')
Set-StrictMode -Version Latest
$ErrorActionPreference='Stop'
$log='C:\Windows\Setup\Scripts\silvermetal-kiosk.log'
function Log($m){ "$(Get-Date -f s) $m" | Add-Content $log }

# Elevating launcher: Shell Launcher runs this as the shell; it relaunches the
# Welcome app elevated (silent via the baked UAC auto-approve).
$launcher='C:\Windows\Setup\Scripts\Start-WelcomeShell.cmd'
@"
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '$WelcomeExe' -Verb RunAs"
:loop
timeout /t 3600 >nul
goto loop
"@ | Set-Content $launcher -Encoding ASCII
Log "wrote launcher $launcher"

# --- Shell Launcher v2 (WMI bridge) ---
$cls='root\standardcimv2\embedded'
$wesl=Get-CimInstance -Namespace $cls -ClassName WESL_UserSetting -ErrorAction Stop
Invoke-CimMethod -Namespace $cls -ClassName WESL_UserSetting -MethodName SetEnabled -Arguments @{Enabled=$true} | Out-Null
# Default shell stays Explorer for everyone else.
Invoke-CimMethod -InputObject $wesl -MethodName SetDefaultShell -Arguments @{Shell='explorer.exe';DefaultAction=[uint32]0} | Out-Null
# sm-bootstrap => the elevating launcher; on exit, restart the shell (action 0).
Invoke-CimMethod -InputObject $wesl -MethodName SetCustomShell -Arguments @{
    Sid=(New-Object System.Security.Principal.NTAccount($BootstrapUser)).Translate([System.Security.Principal.SecurityIdentifier]).Value
    Shell="cmd.exe /c `"$launcher`""
    DefaultAction=[uint32]0
} | Out-Null
Log 'shell launcher configured for sm-bootstrap'

# --- Keyboard Filter (block shell hotkeys) ---
Enable-WindowsOptionalFeature -Online -FeatureName Client-KeyboardFilter -NoRestart -ErrorAction SilentlyContinue | Out-Null
$kf='root\standardcimv2\embedded'
foreach($combo in 'Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R'){
    $p=Get-CimInstance -Namespace $kf -ClassName WEKF_PredefinedKey -Filter "Id='$combo'" -ErrorAction SilentlyContinue
    if($p){ $p.Enabled=$true; Set-CimInstance -InputObject $p }
}
Log 'keyboard filter rules enabled'

# --- escape policies (machine-wide; reverted at teardown) ---
$sys='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
New-Item $sys -Force | Out-Null
Set-ItemProperty $sys -Name DisableTaskMgr        -Value 1 -Type DWord
Set-ItemProperty $sys -Name DisableLockWorkstation -Value 1 -Type DWord
Set-ItemProperty $sys -Name HideFastUserSwitching  -Value 1 -Type DWord
Log 'escape policies set; kiosk ready'
```

- [ ] **Step 2: Parse-lint**

Run: `pwsh -NoProfile -Command "[void][System.Management.Automation.Language.Parser]::ParseFile('windows/installer/oem/Configure-Kiosk.ps1',[ref]$null,[ref]$null); 'OK'"`
Expected: `OK`

- [ ] **Step 3: Commit**

```bash
git add windows/installer/oem/Configure-Kiosk.ps1
git commit -m "feat(kiosk): Configure-Kiosk.ps1 (Shell Launcher v2 + Keyboard Filter + escapes)"
```

---

### Task B2: Enable kiosk features offline in `build.ps1`

**Files:**
- Modify: `windows/installer/build.ps1` (`Invoke-ServiceWim`, after drivers block ~line 193)

- [ ] **Step 1: Enable the optional features in the mounted WIM**

After the drivers block in `Invoke-ServiceWim`, add:
```powershell
        # Kiosk features (Shell Launcher v2 + Keyboard Filter) — IoT Enterprise LTSC.
        if ($env:SILVERMETAL_WELCOME_ENABLED -ne '0') {
            Write-Host '   enabling Shell Launcher + Keyboard Filter features'
            Enable-WindowsOptionalFeature -Path $mount -FeatureName Client-EmbeddedShellLauncher -All -NoRestart | Out-Null
            Enable-WindowsOptionalFeature -Path $mount -FeatureName Client-KeyboardFilter        -All -NoRestart | Out-Null
        }
```

- [ ] **Step 2: Stage `Configure-Kiosk.ps1` alongside the hardening payload**

In the hardening-staging block (~line 210), after copying `SetupComplete.cmd`, add:
```powershell
        Copy-Item (Join-Path $PSScriptRoot 'oem\Configure-Kiosk.ps1') $scripts -Force
```

- [ ] **Step 3: Parse-lint** (same command as Task A5 Step 3) → `OK`

- [ ] **Step 4: Commit**

```bash
git add windows/installer/build.ps1
git commit -m "feat(build): enable kiosk features offline + stage Configure-Kiosk.ps1"
```

---

### Task B3: Invoke kiosk config from `SetupComplete.cmd`

**Files:**
- Modify: `windows/installer/oem/SetupComplete.cmd`

- [ ] **Step 1: Add the kiosk-config call (before the Welcome-present branch)**

Insert after line 15 (`echo [...] first-boot start`):
```bat
if exist "C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe" (
  echo [%DATE% %TIME%] configuring onboarding kiosk >> "%LOG%"
  powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Configure-Kiosk.ps1" >> "%LOG%" 2>&1
)
```

- [ ] **Step 2: Verify the file still has the deferral branch intact** (read it; the existing if/else for hardening deferral must remain).

Run: `pwsh -NoProfile -Command "Get-Content windows/installer/oem/SetupComplete.cmd"`
Expected: kiosk-config block present AND the original hardening-deferral if/else present.

- [ ] **Step 3: Commit**

```bash
git add windows/installer/oem/SetupComplete.cmd
git commit -m "feat(kiosk): configure kiosk from SetupComplete before first logon"
```

---

### Task B4: Remove the redundant `FirstLogonCommands` launch

Shell Launcher now launches the Welcome app as the session shell, so the answer-file launch would double-launch.

**Files:**
- Modify: `windows/installer/autounattend/autounattend.xml:122-128`

- [ ] **Step 1: Delete the `<FirstLogonCommands>` block**

Remove lines 122-128 (the whole `<FirstLogonCommands>…</FirstLogonCommands>` element) and replace the preceding comment (lines 115-121) with:
```xml
      <!--
        The Welcome wizard is launched by Shell Launcher v2 as the sm-bootstrap
        session shell (Configure-Kiosk.ps1, run from SetupComplete.cmd). No
        FirstLogonCommands launch is needed; adding one would double-launch.
      -->
```

- [ ] **Step 2: Validate the XML still parses**

Run: `pwsh -NoProfile -Command "[xml](Get-Content windows/installer/autounattend/autounattend.xml -Raw); 'OK'"`
Expected: `OK`

- [ ] **Step 3: Commit**

```bash
git add windows/installer/autounattend/autounattend.xml
git commit -m "feat(kiosk): drop FirstLogonCommands launch (Shell Launcher owns launch)"
```

---

### Task B5: Kiosk teardown in the Welcome app `ApplyService`

The app already deletes `sm-bootstrap` + removes AutoLogon on success. Add reversal of the kiosk config so the real user gets a normal desktop.

**Files:**
- Modify: the bootstrap/teardown service in `windows/welcome/src/SilverOS.Welcome.Core` (find the class that removes AutoLogon — likely `BootstrapService`).
- Test: existing Core test project (mirror its pattern).

- [ ] **Step 1: Find the teardown code**

Run: `pwsh -NoProfile -Command "Select-String -Path windows/welcome/src/**/*.cs -Pattern 'AutoAdminLogon|sm-bootstrap|DeleteUser|Bootstrap' -List | Select-Object Path"`
Expected: locates `BootstrapService` (or equivalent). Read it to match its `IProcessRunner`/registry idiom.

- [ ] **Step 2: Write a failing test for `RevertKioskAsync` (match the project's existing test style)**

Add to the Core test project a test asserting `BootstrapService.RevertKioskAsync()` invokes: a process that clears the `sm-bootstrap` WESL custom shell, and registry deletes for `DisableTaskMgr`/`DisableLockWorkstation`/`HideFastUserSwitching`. Use the project's existing fake `IProcessRunner` to capture invocations. (Mirror an existing `BootstrapService` test exactly — repeat its arrange/fake setup.)

- [ ] **Step 3: Run to verify failure** → method not defined.

- [ ] **Step 4: Implement `RevertKioskAsync`**

Add to `BootstrapService`:
```csharp
public async Task RevertKioskAsync()
{
    // Remove sm-bootstrap custom shell + disable Shell Launcher's per-user entry.
    await _runner.RunPowerShellAsync(
        "$c='root\\standardcimv2\\embedded';" +
        "$w=Get-CimInstance -Namespace $c -ClassName WESL_UserSetting;" +
        "$sid=(New-Object System.Security.Principal.NTAccount('sm-bootstrap')).Translate([System.Security.Principal.SecurityIdentifier]).Value;" +
        "Invoke-CimMethod -InputObject $w -MethodName RemoveCustomShell -Arguments @{Sid=$sid} -ErrorAction SilentlyContinue;" +
        "Invoke-CimMethod -InputObject $w -MethodName SetEnabled -Arguments @{Enabled=$false} -ErrorAction SilentlyContinue");
    // Revert escape policies.
    await _runner.RunPowerShellAsync(
        "$s='HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System';" +
        "Remove-ItemProperty $s -Name DisableTaskMgr,DisableLockWorkstation,HideFastUserSwitching -ErrorAction SilentlyContinue");
}
```
> If `IProcessRunner` has no `RunPowerShellAsync`, use its existing method (Step 1 told you the real signature) — adapt the two calls to it.

- [ ] **Step 5: Call `RevertKioskAsync` from the success path**

In `ApplyService`, in the teardown sequence (where `sm-bootstrap` is deleted / AutoLogon removed), call `await _bootstrap.RevertKioskAsync();` **before** the reboot and **before** deleting `sm-bootstrap` (the SID must still resolve).

- [ ] **Step 6: Run tests** → PASS. Build the whole solution (`dotnet build` on the welcome `.sln`) → 0 errors.

- [ ] **Step 7: Commit**

```bash
git add windows/welcome
git commit -m "feat(kiosk): revert kiosk (shell launcher + escapes) on wizard success"
```

**Phase B done** → onboarding session is an escape-proof kiosk, reverted for the real user.

---

## Phase C — First-boot presentation (MAUI Welcome app)

### Task C1: Borderless fullscreen window

**Files:**
- Modify: `windows/welcome/src/SilverOS.Welcome.App/App.xaml.cs`
- Create: `windows/welcome/src/SilverOS.Welcome.App/Platforms/Windows/WindowExtensions.cs`

- [ ] **Step 1: Add a Windows-only window customizer**

`Platforms/Windows/WindowExtensions.cs`:
```csharp
#if WINDOWS
using Microsoft.UI.Windowing;
using Microsoft.UI;
using WinRT.Interop;

namespace SilverOS.Welcome.App;

public static class WindowExtensions
{
    // Borderless, fullscreen, non-closable kiosk window.
    public static void ApplyKioskChrome(this Microsoft.UI.Xaml.Window winuiWindow)
    {
        var hwnd = WindowNative.GetWindowHandle(winuiWindow);
        var id = Win32Interop.GetWindowIdFromWindow(hwnd);
        var appWindow = AppWindow.GetFromWindowId(id);
        if (appWindow.Presenter is OverlappedPresenter p)
        {
            p.SetBorderAndTitleBar(false, false);
            p.IsResizable = false; p.IsMaximizable = false; p.IsMinimizable = false;
        }
        appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
        // Block the close box; the wizard exits by rebooting, not by closing.
        appWindow.Closing += (s, e) => e.Cancel = true;
    }
}
#endif
```

- [ ] **Step 2: Call it when the native window is created**

In `App.xaml.cs`, replace `CreateWindow` with one that hooks the handler:
```csharp
protected override Window CreateWindow(IActivationState? activationState)
{
    var window = new Window(new MainPage()) { Title = "SilverMetal Windows" };
#if WINDOWS
    window.HandlerChanged += (s, e) =>
    {
        if (window.Handler?.PlatformView is Microsoft.UI.Xaml.Window native)
            native.ApplyKioskChrome();
    };
#endif
    return window;
}
```

- [ ] **Step 3: Build for Windows**

Run: `dotnet build windows/welcome/src/SilverOS.Welcome.App -f net9.0-windows10.0.19041.0`
Expected: 0 errors.

- [ ] **Step 4: Commit**

```bash
git add windows/welcome/src/SilverOS.Welcome.App
git commit -m "feat(welcome): borderless fullscreen non-closable kiosk window"
```

---

### Task C2: Brand stylesheet (the void/cyan glass system)

**Files:**
- Create: `windows/welcome/src/SilverOS.Welcome.App/wwwroot/css/silvermetal.css`
- Modify: `windows/welcome/src/SilverOS.Welcome.App/wwwroot/index.html` (link the stylesheet)

- [ ] **Step 1: Add the brand stylesheet** (ported from the approved mockups — backdrop wall + frosted glass card + step rail)

`wwwroot/css/silvermetal.css`:
```css
:root{
  --void:#0b0f14; --ink:#e8edf5; --mid:#8fa4bc;
  --accent:#00d4ff; --accent2:#00e5a0; --line:rgba(255,255,255,.08);
}
html,body{height:100%;margin:0;background:#05080c;color:var(--ink);
  font-family:"Segoe UI Variable","Segoe UI",system-ui,sans-serif;overflow:hidden}
.sm-wall{position:fixed;inset:0;background:
  radial-gradient(85% 75% at 28% 18%, #173247, transparent 60%),
  radial-gradient(75% 75% at 82% 92%, #0f3528, transparent 55%),
  linear-gradient(135deg,#080f17,#0a1612)}
.sm-wall::after{content:"SILVERMETAL";position:fixed;right:26px;bottom:18px;
  font:700 12px/1 system-ui;letter-spacing:4px;color:rgba(255,255,255,.18)}
.sm-glass{position:fixed;inset:12% 16%;border-radius:18px;
  background:rgba(16,22,31,.55);backdrop-filter:blur(18px);
  border:1px solid rgba(255,255,255,.14);
  box-shadow:0 24px 70px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.12);
  display:flex;flex-direction:column;overflow:hidden;animation:sm-rise .5s ease both}
@keyframes sm-rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.sm-rail{display:flex;gap:14px;padding:14px 26px;font:600 10px/1 "Cascadia Mono",Consolas,monospace;
  letter-spacing:1px;color:var(--mid);border-bottom:1px solid var(--line)}
.sm-rail .on{color:var(--accent)} .sm-rail .done{color:var(--accent2)}
.sm-body{flex:1;padding:18px 30px;overflow:auto;min-height:0}
.sm-next{align-self:flex-end;margin:14px 26px;background:linear-gradient(180deg,#13b6e6,#0a93c8);
  color:#001018;font-weight:700;border:0;border-radius:9px;padding:10px 22px;cursor:pointer}
```

- [ ] **Step 2: Link it in `index.html`** (after the existing stylesheet links)

```html
<link rel="stylesheet" href="css/silvermetal.css" />
```

- [ ] **Step 3: Build** → 0 errors. Commit.

```bash
git add windows/welcome/src/SilverOS.Welcome.App/wwwroot
git commit -m "feat(welcome): SilverMetal void/cyan glass stylesheet"
```

---

### Task C3: Apply the Hybrid shell to `MainLayout`

**Files:**
- Modify: `windows/welcome/src/SilverOS.Welcome.App/Components/Layout/MainLayout.razor`

- [ ] **Step 1: Replace the stock template layout with the glass shell**

`MainLayout.razor`:
```razor
@inherits LayoutComponentBase

<div class="sm-wall"></div>
<div class="sm-glass">
    <div class="sm-body">
        @Body
    </div>
</div>
```
> The step rail (`sm-rail`) and the Next button (`sm-next`) live inside the wizard step components, which render the per-step content into `@Body`. If a shared step chrome already exists, move the rail here instead — match whatever the existing step components expect (read `Components/Steps` first).

- [ ] **Step 2: Build + run the app on the dev box (windowed sanity check is fine without the kiosk)**

Run: `dotnet build windows/welcome/src/SilverOS.Welcome.App -f net9.0-windows10.0.19041.0`
Expected: 0 errors. (Visual verification happens in the VM e2e, Task D2.)

- [ ] **Step 3: Commit**

```bash
git add windows/welcome/src/SilverOS.Welcome.App/Components
git commit -m "feat(welcome): Hybrid glass-card shell in MainLayout"
```

---

### Task C4: Remove the dead default-template nav

The stock `NavMenu` + the "About → learn.microsoft.com" top row are template cruft that the kiosk must not show.

**Files:**
- Modify: `windows/welcome/src/SilverOS.Welcome.App/Components/Layout/MainLayout.razor` (already replaced — confirms no `<NavMenu/>` / top-row remain)
- Delete: `windows/welcome/src/SilverOS.Welcome.App/Components/Layout/NavMenu.razor` (if nothing else references it)

- [ ] **Step 1: Check for other references to NavMenu**

Run: `pwsh -NoProfile -Command "Select-String -Path windows/welcome/src/**/*.razor -Pattern 'NavMenu'"`
Expected: only the (now-removed) MainLayout — if so, delete `NavMenu.razor`. If referenced elsewhere, leave it and just ensure it's not rendered in the kiosk path.

- [ ] **Step 2: Build** → 0 errors. **Commit.**

```bash
git add -A windows/welcome/src/SilverOS.Welcome.App/Components
git commit -m "chore(welcome): remove stock template nav from kiosk shell"
```

**Phase C done** → Welcome app presents as the fullscreen branded glass card.

---

## Phase D — Integration + VM end-to-end

### Task D1: Build a test ISO with branding + kiosk enabled

**Files:** none (operational). Build host = Windows + ADK (per `iso-builder.md`).

- [ ] **Step 1: Run the pipeline against a licensed base ISO**

Run (elevated, on the Windows runner / ADK box):
```powershell
windows\installer\build.ps1 -SourceIso <path-to-IoT-LTSC.iso> -SkipInputVerify
```
Expected: completes through Stage 7; `out\SilverMetal-Enhanced-Windows.iso` + `.sha256` + `.sbom.json` produced. Branding stage 3e and kiosk feature-enable log lines present.

- [ ] **Step 2: Assert ISO structure**

Run: `pwsh -NoProfile windows/tests/Assert-IsoStructure.ps1 -IsoPath windows/installer/out/SilverMetal-Enhanced-Windows.iso` (or the script's actual parameter).
Expected: PASS.

---

### Task D2: VM boot e2e (uses the existing harness)

**Files:** none (operational). Harness = SLAB01 VM 102 + `_stageiso.py` / `_pverun.py` / `_shot.py` (see memory `silveros-welcome-app-implementation.md`).

- [ ] **Step 1: Stage + boot the ISO in VM 102** (per the established harness flow; eject CD after file-copy to avoid the reinstall loop).

- [ ] **Step 2: Verify the kiosk session** — screenshot the sm-bootstrap session. Assert:
  - The Welcome app fills the screen as the **glass card on the branded wall**.
  - **No taskbar, no Start** is present (Shell Launcher replaced Explorer).
  - Win key / Win+L / Ctrl+Alt+Del → Task Manager are blocked (try via `_pverun`/host key-send; the wizard stays foreground).

- [ ] **Step 3: Complete the wizard** (pick a flavour, create the real user, set BitLocker PIN per existing flow). Let it reboot.

- [ ] **Step 4: Verify the real-user desktop**:
  - Logs into a **normal Explorer desktop** (taskbar back) — kiosk reverted.
  - **Branded wallpaper** + dark theme + cyan accent.
  - **Lock screen** shows the branded image and cannot be changed (Settings ▸ Personalization ▸ Lock screen is greyed / policy-managed).
  - **Settings ▸ System ▸ About** shows Manufacturer `SilverLABS`, Model `SilverMetal Windows`, Support `https://silverlabs.uk`, logo.

- [ ] **Step 5: Verify BitLocker recovery message** (best-effort): trigger recovery (or inspect `manage-bde -status` + the FVE policy) to confirm the custom recovery message/URL applied. If the `FVE` value names from Task A3 proved wrong, fix them now and re-run Phase A tests + rebuild.

- [ ] **Step 6: Record evidence** — capture screenshots into `.superpowers/` (gitignored) and note results in the PR description.

---

### Task D3: Finish the branch

- [ ] **Step 1: Run the full local test suite**

Run: `pwsh -NoProfile -Command "Invoke-Pester windows/tests/Branding.Tests.ps1"` → PASS, and `dotnet build` the welcome `.sln` → 0 errors.

- [ ] **Step 2: Use `superpowers:finishing-a-development-branch`** to open the PR (`feat/first-boot-branding` → `main`), summarizing the three components + VM evidence. Note follow-ups: SilverOS→SilverMetal rename; final brand assets; any `FVE`/`PersonalizationCSP` value-name corrections discovered on the VM.

---

## Self-review notes (author)

- **Spec coverage**: A=branding module (§4) ✓; A5=Invoke-Brand wiring (§7.1) ✓; B=kiosk (§5, §7.2–7.4) ✓; C=presentation (§6, §7.5) ✓; D=testing (§8) ✓. Honest BitLocker limitation (§4) is carried into A3's comment + D2 Step 5. Open items (§9) are attached to the tasks that resolve them (FVE names → A3/D2; PersonalizationCSP → A3/D2; elevation route → B1; assets → A4; rename → D3).
- **Type consistency**: `Set-SmRegValue`, `Set-OemInformation`, `Set-LockScreen`, `Set-DesktopBranding`, `Set-BitLockerPreboot`, `Apply-Branding.ps1 -Mode/-MountPath/-PassThru`, `Configure-Kiosk.ps1`, `RevertKioskAsync`, `ApplyKioskChrome` are used consistently across tasks.
- **Known soft spots flagged in-place (not placeholders)**: exact `FVE` value names and `PersonalizationCSP` reliability are written concretely but guarded by read-back tests + the VM verify; `IProcessRunner` method name is resolved by reading the real class in B5 Step 1.
