Private/Export-WorkspaceTestHtmlReport.ps1

function Export-WorkspaceTestHtmlReport {
    <#
    .SYNOPSIS
        Writes an HTML validation report.
 
    .DESCRIPTION
        Creates a self-contained, human-readable HTML report from
        WorkspaceTestingFramework result objects. Results are sorted by group,
        type, and name.
 
        The report shows a pass/fail verdict banner, system context for the
        validated machine, clickable summary cards that filter by status, a
        text search, and collapsible groups. Failed checks are expanded on
        load. The report has no external dependencies, adapts to light and
        dark color schemes, and includes a print stylesheet.
 
    .PARAMETER Results
        Result objects to include in the report.
 
    .PARAMETER Path
        Destination path for the HTML report.
 
    .OUTPUTS
        None.
 
    .NOTES
        Private report helper.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]]$Results,

        [Parameter(Mandatory)]
        [string]$Path
    )

    $Results = @($Results | Sort-Object -Property Group, Type, Name)
    $directory = Split-Path -Path $Path -Parent
    if ($directory -and -not (Test-Path -LiteralPath $directory)) {
        New-Item -Path $directory -ItemType Directory -Force | Out-Null
    }

    $metaKeys = [System.Collections.Generic.HashSet[string]]([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($k in @('Name', 'Group', 'Tags', 'OnFailure', 'Severity', 'PassWhen', 'FailWhen')) { $metaKeys.Add($k) | Out-Null }

    function ConvertTo-JsonObj {
        param([object]$Value)
        if ($null -eq $Value) { return $null }
        $json = $Value | ConvertTo-Json -Depth 5 -Compress
        try { return $json | ConvertFrom-Json -ErrorAction Stop } catch { return $null }
    }

    function Format-CriteriaRuleText {
        param([object]$Rules)
        if ($null -eq $Rules) { return '' }
        $list = @($Rules)
        $lines = foreach ($rule in $list) {
            $parts = @($rule.PSObject.Properties | ForEach-Object {
                $v = if ($_.Value -is [array]) { $_.Value -join ' | ' } else { $_.Value }
                "$($_.Name) = $v"
            })
            [System.Net.WebUtility]::HtmlEncode($parts -join '; ')
        }
        ($lines | ForEach-Object { "<div class=""criteria-rule"">$_</div>" }) -join ''
    }

    # Maps Actual field names to their Expected equivalent for alignment
    $keyAliases = @{
        'DisplayVersion' = 'Version'
        'DisplayName'    = 'Name'
    }

    function ConvertTo-HtmlDetailTable {
        param([object]$Expected, [object]$Actual, [string]$Status, [string]$OnFailure)
        $expObj = ConvertTo-JsonObj $Expected
        $actObj = ConvertTo-JsonObj $Actual

        # Normalise Actual keys using aliases so they align with Expected rows
        if ($actObj) {
            foreach ($alias in $keyAliases.GetEnumerator()) {
                $actProp = $actObj.PSObject.Properties[$alias.Key]
                if ($actProp -and -not $actObj.PSObject.Properties[$alias.Value]) {
                    $actObj | Add-Member -NotePropertyName $alias.Value -NotePropertyValue $actProp.Value -Force
                    $actObj.PSObject.Properties.Remove($alias.Key)
                }
            }
        }

        # Collect all meaningful keys from both sides, excluding meta
        $allKeys = [System.Collections.Generic.List[string]]::new()
        $seen    = [System.Collections.Generic.HashSet[string]]([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($src in @($expObj, $actObj)) {
            if ($null -eq $src) { continue }
            foreach ($p in $src.PSObject.Properties) {
                if (-not $metaKeys.Contains($p.Name) -and $seen.Add($p.Name)) {
                    $allKeys.Add($p.Name)
                }
            }
        }

        # Field names referenced by PassWhen/FailWhen rules; their expectation
        # lives in the Criteria card, not in a single Expected value
        $criteriaKeys = [System.Collections.Generic.HashSet[string]]([System.StringComparer]::OrdinalIgnoreCase)
        if ($expObj) {
            foreach ($setName in @('PassWhen', 'FailWhen')) {
                $set = $expObj.PSObject.Properties[$setName]
                if ($set -and $null -ne $set.Value) {
                    foreach ($rule in @($set.Value)) {
                        foreach ($p in $rule.PSObject.Properties) { $criteriaKeys.Add($p.Name) | Out-Null }
                    }
                }
            }
        }

        $rows = foreach ($key in $allKeys) {
            $expProp = if ($expObj) { $expObj.PSObject.Properties[$key] } else { $null }
            $actProp = if ($actObj) { $actObj.PSObject.Properties[$key] } else { $null }

            # $expProp present but null means "key exists, no constraint" -> show (any)
            $expVal = if ($null -eq $expProp) { $null }
                      elseif ($null -eq $expProp.Value) { '' }
                      elseif ($expProp.Value -is [array]) { $expProp.Value -join ', ' }
                      else { $expProp.Value.ToString() }

            $actVal = if ($null -eq $actProp) { $null }
                      elseif ($null -eq $actProp.Value) { $null }
                      elseif ($actProp.Value -is [array]) { $actProp.Value -join ', ' }
                      else { $actProp.Value.ToString() }

            # Skip rows where both sides have nothing to show
            if ($null -eq $expVal -and $null -eq $actVal) { continue }
            if ([string]::IsNullOrEmpty($expVal) -and [string]::IsNullOrEmpty($actVal)) { continue }

            $differs = ($null -ne $expVal -and $expVal -ne '' -and $null -ne $actVal -and $expVal -ne $actVal)
            $rowClass = if ($differs -and $Status -ne 'Pass') { ' class="row-diff"' } else { '' }
            $kHtml  = [System.Net.WebUtility]::HtmlEncode($key)
            # Empty string means key present but null value -> show (any) to indicate no constraint.
            # No expectation at all: point to the Criteria card when the field is ruled there.
            $eHtml  = if ($null -eq $expVal) {
                          if ($criteriaKeys.Contains($key)) { '<em class="na">(see criteria)</em>' } else { '<em class="na">&#8212;</em>' }
                      }
                      elseif ($expVal -eq '')   { '<em class="na">(any)</em>' }
                      else                      { [System.Net.WebUtility]::HtmlEncode($expVal) }
            $aHtml  = if ($null -ne $actVal)   { [System.Net.WebUtility]::HtmlEncode($actVal) } else { '<em class="na">&#8212;</em>' }
            "<tr$rowClass><td class=""dk"">$kHtml</td><td>$eHtml</td><td>$aHtml</td></tr>"
        }

        # Side-by-side cards: comparison, criteria, and settings share the panel width
        $cards = [System.Collections.Generic.List[string]]::new()

        $rowsHtml = ($rows -join '')
        if ($rowsHtml) {
            $cards.Add("<div class=""detail-card""><div class=""detail-card-title"">Expected vs actual</div><table class=""cmp""><thead><tr><th></th><th>Expected</th><th>Actual</th></tr></thead><tbody>$rowsHtml</tbody></table></div>")
        }

        # PassWhen / FailWhen criteria (services, scheduled tasks)
        if ($expObj) {
            $pw = $expObj.PSObject.Properties['PassWhen']
            $fw = $expObj.PSObject.Properties['FailWhen']
            if ($pw -or $fw) {
                $criteriaRows = ''
                if ($pw) { $criteriaRows += "<tr><td class=""dk"">PassWhen</td><td>$(Format-CriteriaRuleText $pw.Value)</td></tr>" }
                if ($fw) { $criteriaRows += "<tr><td class=""dk"">FailWhen</td><td>$(Format-CriteriaRuleText $fw.Value)</td></tr>" }
                $cards.Add("<div class=""detail-card""><div class=""detail-card-title"">Criteria</div><table class=""cmp""><tbody>$criteriaRows</tbody></table></div>")
            }
        }

        if ($OnFailure) {
            $of = [System.Net.WebUtility]::HtmlEncode($OnFailure)
            $cards.Add("<div class=""detail-card""><div class=""detail-card-title"">Settings</div><table class=""cmp""><tbody><tr><td class=""dk"">On Failure</td><td>$of</td></tr></tbody></table></div>")
        }

        if ($cards.Count -eq 0) { return '' }
        "<div class=""detail-cols"">$($cards -join '')</div>"
    }

    $summary = [PSCustomObject]@{
        Total            = @($Results).Count
        Pass             = @($Results | Where-Object Status -eq 'Pass').Count
        Fail             = @($Results | Where-Object Status -eq 'Fail').Count
        Warning          = @($Results | Where-Object Status -eq 'Warning').Count
        Skipped          = @($Results | Where-Object Status -eq 'Skipped').Count
        NotApplicable    = @($Results | Where-Object Status -eq 'NotApplicable').Count
        BlockingFailures = @($Results | Where-Object { $_.Status -eq 'Fail' -and $_.OnFailure -eq 'Fail' }).Count
    }

    # Verdict banner
    if ($summary.Total -eq 0) {
        $verdictClass = 'verdict-none'
        $verdictText = 'NO CHECKS'
        $verdictSub = 'The baseline produced no results.'
    } elseif ($summary.BlockingFailures -gt 0) {
        $verdictClass = 'verdict-fail'
        $verdictText = 'FAILED'
        $verdictSub = '{0} blocking failure{1} out of {2} checks.' -f $summary.BlockingFailures, $(if ($summary.BlockingFailures -eq 1) { '' } else { 's' }), $summary.Total
    } else {
        $verdictClass = 'verdict-pass'
        $verdictText = 'PASSED'
        $verdictSub = '{0} of {1} checks passed.' -f $summary.Pass, $summary.Total
        if ($summary.Warning -gt 0) {
            $verdictSub += ' {0} warning{1}.' -f $summary.Warning, $(if ($summary.Warning -eq 1) { '' } else { 's' })
        }
    }

    # System context for the report header (best effort, never fatal)
    $context = try { Get-WorkspaceTestSystemContext } catch { $null }
    $moduleVersion = if ($MyInvocation.MyCommand.Module) { [string]$MyInvocation.MyCommand.Module.Version } else { '' }
    $baselineNames = @(
        $Results |
            ForEach-Object { $_.Baseline } |
            Where-Object { $_ -and $_.Name } |
            ForEach-Object { if ($_.Version) { '{0} {1}' -f $_.Name, $_.Version } else { [string]$_.Name } } |
            Select-Object -Unique
    )
    $sources = @($Results | ForEach-Object { $_.Source } | Where-Object { $_ } | Select-Object -Unique)

    $metaItems = [System.Collections.Generic.List[string]]::new()
    function Add-MetaItem {
        param([string]$Label, [string]$Value)
        if ([string]::IsNullOrWhiteSpace($Value)) { return }
        $l = [System.Net.WebUtility]::HtmlEncode($Label)
        $v = [System.Net.WebUtility]::HtmlEncode($Value)
        $metaItems.Add("<div class=""meta-item""><span class=""meta-label"">$l</span><span class=""meta-value"">$v</span></div>")
    }
    Add-MetaItem 'Machine' $env:COMPUTERNAME
    if ($context) {
        Add-MetaItem 'Operating system' ('{0} (build {1})' -f $context.Caption, $context.BuildNumber)
        Add-MetaItem 'Platform' $context.Platform
        Add-MetaItem 'Hardware' (('{0} {1}' -f $context.Manufacturer, $context.Model).Trim())
    }
    Add-MetaItem 'User' ('{0}\{1}' -f $env:USERDOMAIN, $env:USERNAME)
    Add-MetaItem 'Module version' $moduleVersion
    Add-MetaItem 'Baseline' ($baselineNames -join ', ')
    Add-MetaItem 'Source' ($sources -join ', ')
    Add-MetaItem 'Generated' (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')

    $groups = $Results | Group-Object -Property Group | Sort-Object -Property Name
    $sections = foreach ($group in $groups) {
        $displayName = if ([string]::IsNullOrWhiteSpace($group.Name)) { 'General' } else { $group.Name }
        $groupName = [System.Net.WebUtility]::HtmlEncode($displayName)
        $groupFail = @($group.Group | Where-Object Status -eq 'Fail').Count
        $groupWarn = @($group.Group | Where-Object Status -eq 'Warning').Count
        $groupOpen = if ($groupFail -gt 0 -or $groupWarn -gt 0) { ' open' } else { '' }

        $chips = "<span class=""chip"">$(@($group.Group).Count) checks</span>"
        if ($groupFail -gt 0) { $chips += "<span class=""chip chip-fail"">$groupFail failed</span>" }
        if ($groupWarn -gt 0) { $chips += "<span class=""chip chip-warn"">$groupWarn warning$(if ($groupWarn -eq 1) { '' } else { 's' })</span>" }

        $tableRows = foreach ($result in ($group.Group | Sort-Object Type, Name)) {
            $detail   = ConvertTo-HtmlDetailTable -Expected $result.Expected -Actual $result.Actual -Status $result.Status -OnFailure $result.OnFailure
            $message  = [System.Net.WebUtility]::HtmlEncode($result.Message)
            $name     = [System.Net.WebUtility]::HtmlEncode($result.Name)
            $type     = [System.Net.WebUtility]::HtmlEncode($result.Type)
            $status   = [System.Net.WebUtility]::HtmlEncode($result.Status)
            $blocking = if ($result.Status -eq 'Fail' -and $result.OnFailure -eq 'Fail') { '1' } else { '0' }

            $tagChips = (@($result.Tags) | Where-Object { $_ } | ForEach-Object { "<span class=""tag"">$([System.Net.WebUtility]::HtmlEncode([string]$_))</span>" }) -join ''
            $sourceHtml = if ($result.Source) { "<span class=""detail-src"">Source: $([System.Net.WebUtility]::HtmlEncode([string]$result.Source))</span>" } else { '' }
            $detailMeta = if ($sourceHtml -or $tagChips) { "<div class=""detail-meta"">$sourceHtml$tagChips</div>" } else { '' }

            $hasDetail = [bool]($detail -or $detailMeta)
            $expandClass = if ($hasDetail) { ' expandable' } else { '' }
            $detailRow = if ($hasDetail) {
                "<tr class=""detail-row"" hidden><td colspan=""4""><div class=""detail-panel"">$detailMeta$detail</div></td></tr>"
            } else { '' }
            "<tr class=""status-$($result.Status)$expandClass"" data-status=""$($result.Status)"" data-blocking=""$blocking""><td><span class=""badge badge-$($result.Status)"">$status</span></td><td>$type</td><td>$name</td><td>$message</td></tr>$detailRow"
        }
        @"
  <details class="group-section"$groupOpen>
    <summary class="group-heading"><span class="group-name">$groupName</span>$chips</summary>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>Status</th><th>Type</th><th>Name</th><th>Message</th></tr>
        </thead>
        <tbody>
          $($tableRows -join [Environment]::NewLine)
        </tbody>
      </table>
    </div>
  </details>
"@

    }

    $html = @"
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Workspace Testing Framework Report</title>
  <style>
    :root {
      --bg: #f0f2f5;
      --surface: #ffffff;
      --surface-alt: #f6f8fa;
      --surface-inset: #eaeef2;
      --text: #1f2328;
      --text-strong: #0d1117;
      --muted: #57606a;
      --faint: #adb5bd;
      --border: #d0d7de;
      --border-soft: #eaeef2;
      --shadow: 0 1px 3px rgba(0,0,0,.08);
      --pass: #1a7f37; --pass-bg: #dafbe1; --pass-fg: #116329;
      --fail: #cf222e; --fail-bg: #ffebe9; --fail-fg: #a40e26;
      --warn: #bf8700; --warn-bg: #fff8c5; --warn-fg: #7d4e00;
      --skip: #57606a; --skip-bg: #eaeef2; --skip-fg: #424a53;
      --na: #8250df; --na-bg: #fbefff; --na-fg: #5a32a3;
      --diff-exp: #fff8c5;
      --diff-act: #ffebe9;
    }
    @media (prefers-color-scheme: dark) {
      :root {
        --bg: #0d1117;
        --surface: #161b22;
        --surface-alt: #1c2129;
        --surface-inset: #21262d;
        --text: #e6edf3;
        --text-strong: #f0f6fc;
        --muted: #9ba6b2;
        --faint: #6e7681;
        --border: #30363d;
        --border-soft: #21262d;
        --shadow: 0 1px 3px rgba(0,0,0,.4);
        --pass: #3fb950; --pass-bg: #12261e; --pass-fg: #56d364;
        --fail: #f85149; --fail-bg: #2d1417; --fail-fg: #ff7b72;
        --warn: #d29922; --warn-bg: #2a2111; --warn-fg: #e3b341;
        --skip: #8b949e; --skip-bg: #21262d; --skip-fg: #b1bac4;
        --na: #a371f7; --na-bg: #221a32; --na-fg: #bc8cff;
        --diff-exp: #2a2111;
        --diff-act: #2d1417;
      }
    }
 
    *, *::before, *::after { box-sizing: border-box; }
    body {
      font-family: 'Segoe UI', system-ui, Arial, sans-serif;
      margin: 0; padding: 32px 40px 60px;
      background: var(--bg); color: var(--text);
    }
    header { margin-bottom: 20px; }
    header h1 { margin: 0 0 4px; font-size: 22px; font-weight: 700; color: var(--text-strong); }
 
    /* -- Verdict banner -- */
    .verdict {
      display: flex; align-items: baseline; gap: 14px;
      border-radius: 10px; padding: 14px 20px; margin-bottom: 20px;
      box-shadow: var(--shadow);
    }
    .verdict strong { font-size: 24px; font-weight: 800; letter-spacing: .03em; }
    .verdict span { font-size: 13px; }
    .verdict-pass { background: var(--pass-bg); color: var(--pass-fg); border-left: 6px solid var(--pass); }
    .verdict-fail { background: var(--fail-bg); color: var(--fail-fg); border-left: 6px solid var(--fail); }
    .verdict-none { background: var(--skip-bg); color: var(--skip-fg); border-left: 6px solid var(--skip); }
 
    /* -- Context meta -- */
    .meta-grid {
      display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
      gap: 8px 20px; background: var(--surface); border-radius: 10px;
      padding: 14px 20px; margin-bottom: 20px; box-shadow: var(--shadow);
    }
    .meta-item { display: flex; flex-direction: column; min-width: 0; }
    .meta-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
    .meta-value { font-size: 13px; color: var(--text); overflow-wrap: anywhere; }
 
    /* -- Summary cards (clickable filters) -- */
    .summary {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
      gap: 12px; margin-bottom: 14px;
    }
    .metric {
      background: var(--surface); border-radius: 10px;
      padding: 14px 14px 10px;
      box-shadow: var(--shadow);
      border: 1px solid transparent;
      border-top: 4px solid var(--border);
      cursor: pointer; user-select: none;
      transition: transform .06s ease;
    }
    .metric:hover { transform: translateY(-1px); }
    .metric.active { border-color: var(--muted); border-top-width: 4px; outline: 2px solid var(--muted); outline-offset: -1px; }
    .metric strong { display: block; font-size: 26px; font-weight: 700; line-height: 1; margin-bottom: 4px; color: var(--text-strong); }
    .metric span { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
    .metric.m-pass { border-top-color: var(--pass); } .metric.m-pass strong { color: var(--pass); }
    .metric.m-fail { border-top-color: var(--fail); } .metric.m-fail strong { color: var(--fail); }
    .metric.m-warn { border-top-color: var(--warn); } .metric.m-warn strong { color: var(--warn); }
    .metric.m-skip { border-top-color: var(--skip); } .metric.m-skip strong { color: var(--skip); }
    .metric.m-na { border-top-color: var(--na); } .metric.m-na strong { color: var(--na); }
    .metric.m-block { border-top-color: var(--fail); } .metric.m-block strong { color: var(--fail); }
 
    /* -- Toolbar -- */
    .toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
    .toolbar input[type="search"] {
      flex: 1 1 260px; max-width: 420px;
      padding: 8px 12px; font-size: 13px; font-family: inherit;
      color: var(--text); background: var(--surface);
      border: 1px solid var(--border); border-radius: 8px;
    }
    .toolbar input[type="search"]:focus { outline: 2px solid var(--muted); outline-offset: -1px; }
    .toolbar button {
      padding: 8px 14px; font-size: 12px; font-family: inherit; font-weight: 600;
      color: var(--muted); background: var(--surface);
      border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
    }
    .toolbar button:hover { background: var(--surface-alt); }
    .filter-hint { font-size: 12px; color: var(--muted); }
 
    /* -- Group sections -- */
    details.group-section { margin-bottom: 16px; }
    summary.group-heading {
      display: flex; align-items: center; gap: 10px;
      font-size: 14px; font-weight: 700; color: var(--text-strong);
      padding: 10px 14px; cursor: pointer; user-select: none;
      background: var(--surface); border-radius: 10px; box-shadow: var(--shadow);
      position: sticky; top: 0; z-index: 2;
      list-style: none;
    }
    summary.group-heading::-webkit-details-marker { display: none; }
    summary.group-heading::before { content: '\25B8'; font-size: 11px; color: var(--muted); }
    details[open] > summary.group-heading::before { content: '\25BE'; }
    details[open] > summary.group-heading { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
    .group-name { flex: 0 1 auto; }
    .chip {
      font-size: 11px; font-weight: 600; color: var(--muted);
      background: var(--surface-inset); border-radius: 12px; padding: 2px 9px;
    }
    .chip-fail { background: var(--fail-bg); color: var(--fail-fg); }
    .chip-warn { background: var(--warn-bg); color: var(--warn-fg); }
 
    /* -- Table wrapper -- */
    .table-wrap {
      background: var(--surface); border-radius: 0 0 10px 10px;
      box-shadow: var(--shadow); overflow-x: auto;
    }
    table { border-collapse: collapse; width: 100%; font-size: 13px; }
    thead tr { background: var(--surface-alt); }
    th { padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 600;
         text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
         border-bottom: 2px solid var(--border); white-space: nowrap; }
    td { padding: 9px 12px; border-bottom: 1px solid var(--border-soft); vertical-align: top; }
    tbody tr:last-child td { border-bottom: none; }
    tbody tr:hover:not(.detail-row) { background: var(--surface-alt); }
 
    /* -- Row left-border accent -- */
    .status-Pass td:first-child { border-left: 3px solid var(--pass); }
    .status-Fail td:first-child { border-left: 3px solid var(--fail); }
    .status-Warning td:first-child { border-left: 3px solid var(--warn); }
    .status-Skipped td:first-child { border-left: 3px solid var(--skip); }
    .status-NotApplicable td:first-child { border-left: 3px solid var(--na); }
 
    /* -- Expandable rows -- */
    tr.expandable { cursor: pointer; }
    tr.expandable td:last-child::after {
      content: ' \25B8'; font-size: 10px; color: var(--muted); float: right; margin-top: 2px;
    }
    tr.expandable.open td:last-child::after { content: ' \25BE'; }
    tr.detail-row td { padding: 0; border-bottom: 1px solid var(--border-soft); background: var(--surface-alt); }
    .detail-panel { padding: 12px 16px 14px; }
    .detail-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
    .detail-src { font-size: 11px; color: var(--muted); margin-right: 8px; overflow-wrap: anywhere; }
    .tag {
      font-size: 10px; font-weight: 600; color: var(--muted);
      background: var(--surface-inset); border: 1px solid var(--border);
      border-radius: 10px; padding: 1px 8px;
    }
 
    /* -- Comparison table -- */
    table.cmp { border-collapse: collapse; font-size: 12px; width: auto; }
    table.cmp thead tr { background: var(--surface-inset); }
    table.cmp th { padding: 4px 8px; font-size: 10px; font-weight: 700; text-transform: uppercase;
                   letter-spacing: .05em; color: var(--muted); border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }
    table.cmp td { padding: 3px 8px; border-bottom: 1px solid var(--border-soft); vertical-align: top; white-space: nowrap; }
    table.cmp tr:last-child td { border-bottom: none; }
    td.dk { font-weight: 600; color: var(--muted); white-space: nowrap; padding-right: 10px; }
    tr.row-diff td:nth-child(2) { background: var(--diff-exp); }
    tr.row-diff td:nth-child(3) { background: var(--diff-act); }
    .detail-cols { display: flex; flex-wrap: wrap; gap: 12px 36px; align-items: flex-start; }
    .detail-card { min-width: 0; }
    .detail-card-title { font-size: 10px; font-weight: 700; text-transform: uppercase;
                         letter-spacing: .05em; color: var(--muted); margin: 0 0 4px; }
    .criteria-rule { font-size: 12px; color: var(--text); padding: 1px 0; }
    em.na { color: var(--faint); font-style: normal; }
 
    /* -- Status badge -- */
    .badge {
      display: inline-block; padding: 2px 9px; border-radius: 12px;
      font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    }
    .badge-Pass { background: var(--pass-bg); color: var(--pass-fg); }
    .badge-Fail { background: var(--fail-bg); color: var(--fail-fg); }
    .badge-Warning { background: var(--warn-bg); color: var(--warn-fg); }
    .badge-Skipped { background: var(--skip-bg); color: var(--skip-fg); }
    .badge-NotApplicable { background: var(--na-bg); color: var(--na-fg); }
 
    /* -- Print -- */
    @media print {
      body { padding: 0; background: #fff; color: #000; }
      .toolbar { display: none; }
      .metric, .verdict, .meta-grid, .table-wrap, summary.group-heading { box-shadow: none; border: 1px solid #ccc; }
      .metric { cursor: default; }
      summary.group-heading { position: static; }
      details.group-section:not([open]) > .table-wrap { display: block; }
      tr.detail-row[hidden] { display: table-row; }
      tr[data-status][hidden] { display: table-row; }
      tr.expandable td:last-child::after { content: ''; }
    }
  </style>
</head>
<body>
  <header>
    <h1>Workspace Testing Framework Report</h1>
  </header>
 
  <div class="verdict $verdictClass"><strong>$verdictText</strong><span>$verdictSub</span></div>
 
  <section class="meta-grid">
    $($metaItems -join [Environment]::NewLine)
  </section>
 
  <section class="summary">
    <div class="metric" data-filter=""> <strong>$($summary.Total)</strong><span>Total</span></div>
    <div class="metric m-pass" data-filter="Pass"> <strong>$($summary.Pass)</strong><span>Pass</span></div>
    <div class="metric m-fail" data-filter="Fail"> <strong>$($summary.Fail)</strong><span>Fail</span></div>
    <div class="metric m-warn" data-filter="Warning"> <strong>$($summary.Warning)</strong><span>Warning</span></div>
    <div class="metric m-skip" data-filter="Skipped"> <strong>$($summary.Skipped)</strong><span>Skipped</span></div>
    <div class="metric m-na" data-filter="NotApplicable"> <strong>$($summary.NotApplicable)</strong><span>Not applicable</span></div>
    <div class="metric m-block" data-filter="Blocking"> <strong>$($summary.BlockingFailures)</strong><span>Blocking</span></div>
  </section>
 
  <div class="toolbar">
    <input type="search" id="search" placeholder="Search name or message..." aria-label="Search checks">
    <button type="button" id="expand-all">Expand all</button>
    <button type="button" id="collapse-all">Collapse all</button>
    <span class="filter-hint" id="filter-hint"></span>
  </div>
 
  $($sections -join [Environment]::NewLine)
  <script>
    (function() {
      var activeFilter = '';
      var searchBox = document.getElementById('search');
      var hint = document.getElementById('filter-hint');
 
      function setDetailOpen(row, open) {
        var detail = row.nextElementSibling;
        if (!detail || !detail.classList.contains('detail-row')) { return; }
        if (open) { detail.removeAttribute('hidden'); row.classList.add('open'); }
        else { detail.setAttribute('hidden', ''); row.classList.remove('open'); }
      }
 
      function applyFilters() {
        var q = searchBox.value.toLowerCase();
        var visibleCount = 0;
        document.querySelectorAll('tr[data-status]').forEach(function(row) {
          var okStatus = !activeFilter ||
            (activeFilter === 'Blocking' ? row.dataset.blocking === '1' : row.dataset.status === activeFilter);
          var okText = !q || row.textContent.toLowerCase().indexOf(q) !== -1;
          var visible = okStatus && okText;
          if (visible) { visibleCount++; row.removeAttribute('hidden'); }
          else { row.setAttribute('hidden', ''); setDetailOpen(row, false); }
        });
        document.querySelectorAll('details.group-section').forEach(function(g) {
          var any = g.querySelector('tbody tr[data-status]:not([hidden])');
          g.style.display = any ? '' : 'none';
          if ((activeFilter || q) && any) { g.open = true; }
        });
        hint.textContent = (activeFilter || q) ? 'Showing ' + visibleCount + ' checks' : '';
      }
 
      document.querySelectorAll('.metric[data-filter]').forEach(function(card) {
        card.addEventListener('click', function() {
          var filter = card.dataset.filter;
          activeFilter = (activeFilter === filter) ? '' : filter;
          document.querySelectorAll('.metric[data-filter]').forEach(function(c) {
            c.classList.toggle('active', c.dataset.filter === activeFilter && activeFilter !== '');
          });
          applyFilters();
        });
      });
 
      searchBox.addEventListener('input', applyFilters);
 
      document.getElementById('expand-all').addEventListener('click', function() {
        document.querySelectorAll('details.group-section').forEach(function(g) { g.open = true; });
        document.querySelectorAll('tr.expandable:not([hidden])').forEach(function(row) { setDetailOpen(row, true); });
      });
      document.getElementById('collapse-all').addEventListener('click', function() {
        document.querySelectorAll('details.group-section').forEach(function(g) { g.open = false; });
        document.querySelectorAll('tr.expandable').forEach(function(row) { setDetailOpen(row, false); });
      });
 
      document.querySelectorAll('tr.expandable').forEach(function(row) {
        row.addEventListener('click', function() {
          var detail = row.nextElementSibling;
          if (detail && detail.classList.contains('detail-row')) {
            setDetailOpen(row, detail.hasAttribute('hidden'));
          }
        });
      });
 
      // Failures are why the report gets opened: expand them on load
      document.querySelectorAll('tr.status-Fail.expandable').forEach(function(row) {
        setDetailOpen(row, true);
      });
 
      // Open every group while printing, restore afterwards
      var printState = [];
      window.addEventListener('beforeprint', function() {
        printState = [];
        document.querySelectorAll('details.group-section').forEach(function(g) {
          printState.push(g.open);
          g.open = true;
        });
      });
      window.addEventListener('afterprint', function() {
        document.querySelectorAll('details.group-section').forEach(function(g, i) {
          if (i < printState.length) { g.open = printState[i]; }
        });
      });
    })();
  </script>
</body>
</html>
"@


    Set-Content -LiteralPath $Path -Value $html -Encoding UTF8
}

# SIG # Begin signature block
# MIInigYJKoZIhvcNAQcCoIInezCCJ3cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCJcw16F/gwC5E1
# FC4OOFP+WeUs6p3bNaMSxTWva3O09aCCIR0wggZFMIIELaADAgECAhAIMk+dt9qR
# b2Pk8qM8Xl1RMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQK
# ExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2Rl
# IFNpZ25pbmcgMjAyMSBDQTAeFw0yNDA0MDQxNDA0MjRaFw0yNzA0MDQxNDA0MjNa
# MGsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlTY2hpam5kZWwxIzAhBgNVBAoMGkpv
# aG4gQmlsbGVrZW5zIENvbnN1bHRhbmN5MSMwIQYDVQQDDBpKb2huIEJpbGxla2Vu
# cyBDb25zdWx0YW5jeTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAMsl
# ntDbSQwHZXwFhmibivbnd0Qfn6sqe/6fos3pKzKxEsR907RkDMet2x6RRg3eJkiI
# r3TFPwqBooyXXgK3zxxpyhGOcuIqyM9J28DVf4kUyZHsjGO/8HFjrr3K1hABNUsz
# P0o7H3o6J31eqV1UmCXYhQlNoW9FOmRC1amlquBmh7w4EKYEytqdmdOBavAD5Xq4
# vLPxNP6kyA+B2YTtk/xM27TghtbwFGKnu9Vwnm7dFcpLxans4ONt2OxDQOMA5Nwg
# cUv/YTpjhq9qoz6ivG55NRJGNvUXsM3w2o7dR6Xh4MuEGrTSrOWGg2A5EcLH1XqQ
# tkF5cZnAPM8W/9HUp8ggornWnFVQ9/6Mga+ermy5wy5XrmQpN+x3u6tit7xlHk1H
# c+4XY4a4ie3BPXG2PhJhmZAn4ebNSBwNHh8z7WTT9X9OFERepGSytZVeEP7hgypt
# SLcuhpwWeR4QdBb7dV++4p3PsAUQVHFpwkSbrRTv4EiJ0Lcz9P1HPGFoHiFAQQID
# AQABo4IBeDCCAXQwDAYDVR0TAQH/BAIwADA9BgNVHR8ENjA0MDKgMKAuhixodHRw
# Oi8vY2NzY2EyMDIxLmNybC5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNybDBzBggrBgEF
# BQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0
# dW0uY29tMDUGCCsGAQUFBzAChilodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y2NzY2EyMDIxLmNlcjAfBgNVHSMEGDAWgBTddF1MANt7n6B0yrFu9zzAMsBwzTAd
# BgNVHQ4EFgQUO6KtBpOBgmrlANVAnyiQC6W6lJwwSwYDVR0gBEQwQjAIBgZngQwB
# BAEwNgYLKoRoAYb2dwIFAQQwJzAlBggrBgEFBQcCARYZaHR0cHM6Ly93d3cuY2Vy
# dHVtLnBsL0NQUzATBgNVHSUEDDAKBggrBgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4Aw
# DQYJKoZIhvcNAQELBQADggIBAEQsN8wgPMdWVkwHPPTN+jKpdns5AKVFjcn00psf
# 2NGVVgWWNQBIQc9lEuTBWb54IK6Ga3hxQRZfnPNo5HGl73YLmFgdFQrFzZ1lnaMd
# Icyh8LTWv6+XNWfoyCM9wCp4zMIDPOs8LKSMQqA/wRgqiACWnOS4a6fyd5GUIAm4
# CuaptpFYr90l4Dn/wAdXOdY32UhgzmSuxpUbhD8gVJUaBNVmQaRqeU8y49MxiVrU
# KJXde1BCrtR9awXbqembc7Nqvmi60tYKlD27hlpKtj6eGPjkht0hHEsgzU0Fxw7Z
# JghYG2wXfpF2ziN893ak9Mi/1dmCNmorGOnybKYfT6ff6YTCDDNkod4egcMZdOSv
# +/Qv+HAeIgEvrxE9QsGlzTwbRtbm6gwYYcVBs/SsVUdBn/TSB35MMxRhHE5iC3aU
# TkDbceo/XP3uFhVL4g2JZHpFfCSu2TQrrzRn2sn07jfMvzeHArCOJgBW1gPqR3Wr
# J4hUxL06Rbg1gs9tU5HGGz9KNQMfQFQ70Wz7UIhezGcFcRfkIfSkMmQYYpsc7rfz
# j+z0ThfDVzzJr2dMOFsMlfj1T6l22GBq9XQx0A4lcc5Fl9pRxbOuHHWFqIBD/BCE
# hwniOCySzqENd2N+oz8znKooSISStnkNaYXt6xblJF2dx9Dn89FK7d1IquNxOwt0
# tI5dMIIGgjCCBGqgAwIBAgIQNsKwvXwbOuejs902y8l1aDANBgkqhkiG9w0BAQwF
# ADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcT
# C0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAs
# BgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcN
# MjEwMzIyMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjBXMQswCQYDVQQGEwJHQjEYMBYG
# A1UEChMPU2VjdGlnbyBMaW1pdGVkMS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBU
# aW1lIFN0YW1waW5nIFJvb3QgUjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAiJ3YuUVnnR3d6LkmgZpUVMB8SQWbzFoVD9mUEES0QUCBdxSZqdTkdizI
# CFNeINCSJS+lV1ipnW5ihkQyC0cRLWXUJzodqpnMRs46npiJPHrfLBOifjfhpdXJ
# 2aHHsPHggGsCi7uE0awqKggE/LkYw3sqaBia67h/3awoqNvGqiFRJ+OTWYmUCO2G
# AXsePHi+/JUNAax3kpqstbl3vcTdOGhtKShvZIvjwulRH87rbukNyHGWX5tNK/WA
# BKf+Gnoi4cmisS7oSimgHUI0Wn/4elNd40BFdSZ1EwpuddZ+Wr7+Dfo0lcHflm/F
# DDrOJ3rWqauUP8hsokDoI7D/yUVI9DAE/WK3Jl3C4LKwIpn1mNzMyptRwsXKrop0
# 6m7NUNHdlTDEMovXAIDGAvYynPt5lutv8lZeI5w3MOlCybAZDpK3Dy1MKo+6aEtE
# 9vtiTMzz/o2dYfdP0KWZwZIXbYsTIlg1YIetCpi5s14qiXOpRsKqFKqav9R1R5vj
# 3NgevsAsvxsAnI8Oa5s2oy25qhsoBIGo/zi6GpxFj+mOdh35Xn91y72J4RGOJEoq
# zEIbW3q0b2iPuWLA911cRxgY5SJYubvjay3nSMbBPPFsyl6mY4/WYucmyS9lo3l7
# jk27MAe145GWxK4O3m3gEFEIkv7kRmefDR7Oe2T1HxAnICQvr9sCAwEAAaOCARYw
# ggESMB8GA1UdIwQYMBaAFFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBT2
# d2rdP/0BE/8WoWyCAi/QCj0UJTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUw
# AwEB/zATBgNVHSUEDDAKBggrBgEFBQcDCDARBgNVHSAECjAIMAYGBFUdIAAwUAYD
# VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz
# dFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMDUGCCsGAQUFBwEBBCkwJzAl
# BggrBgEFBQcwAYYZaHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0B
# AQwFAAOCAgEADr5lQe1oRLjlocXUEYfktzsljOt+2sgXke3Y8UPEooU5y39rAARa
# AdAxUeiX1ktLJ3+lgxtoLQhn5cFb3GF2SSZRX8ptQ6IvuD3wz/LNHKpQ5nX8hjsD
# LRhsyeIiJsms9yAWnvdYOdEMq1W61KE9JlBkB20XBee6JaXx4UBErc+YuoSb1SxV
# f7nkNtUjPfcxuFtrQdRMRi/fInV/AobE8Gw/8yBMQKKaHt5eia8ybT8Y/Ffa6HAJ
# yz9gvEOcF1VWXG8OMeM7Vy7Bs6mSIkYeYtddU1ux1dQLbEGur18ut97wgGwDiGin
# CwKPyFO7ApcmVJOtlw9FVJxw/mL1TbyBns4zOgkaXFnnfzg4qbSvnrwyj1NiurMp
# 4pmAWjR+Pb/SIduPnmFzbSN/G8reZCL4fvGlvPFk4Uab/JVCSmj59+/mB2Gn6G/U
# YOy8k60mKcmaAZsEVkhOFuoj4we8CYyaR9vd9PGZKSinaZIkvVjbH/3nlLb0a7SB
# IkiRzfPfS9T+JesylbHa1LtRV9U/7m0q7Ma2CQ/t392ioOssXW7oKLdOmMBl14su
# VFBmbzrt5V5cQPnwtd3UOTpS9oCG+ZZheiIvPgkDmA8FzPsnfXW5qHELB43ET7HH
# FHeRPRYrMBKjkb8/IN7Po0d0hQoF4TeMM+zYAJzoKQnVKOLg8pZVPT8wgganMIIE
# j6ADAgECAhEAkKwIciD9xafEa1zHDfc9BjANBgkqhkiG9w0BAQwFADBXMQswCQYD
# VQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS4wLAYDVQQDEyVTZWN0
# aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFJvb3QgUjQ2MB4XDTI2MDMyNTAwMDAw
# MFoXDTQxMDMyNDIzNTk1OVowVTELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3Rp
# Z28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJsaWMgVGltZSBTdGFtcGlu
# ZyBDQSBSNDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCu5EqiAa2C
# HGL5Zi1bmgPM8NUXwYZJ+BtQqHps43GLTC+sjVLypsBh+8uv+TLkgtVGD//vSmA0
# qrzELf9YRCh2MTAA/aGaQZKGg0BRCmziR3pbCnvgWjtGXBDUyn3j3K2lZAO8KxgF
# tlxwOYEAkL+CCqK4v9zzTl8ZwzDpPMiDIFa5THk8an1ieF5I09cXNrPQw+1ER1li
# ThaG0z6FrOpqwxZWmPRZQBw2E32878UB1bL0Zp91vuWZgsMpNNiPCoBj0/1F+LE8
# +NRokfqacFI0F2tftrRB2W7HQClLR9zjxFbWb5be2rceIfNyHUUfKGIvMI2NzoxS
# lxXnFqUG887D8W1Cj8DFok688JKxWvHR/9aQykSbd+9Vutj36ij2sgq/125wTpUZ
# /AgC0ph50bRs7gFrUyaXE9wSsOqMvCCC+sEm7vd/BemSG0TSHNXSmyCba+FCzeke
# WX03TRIcF3Laqd0Rw24OH7jpei4zaGhcI7nfdhBA4c8RScxNY6jeHLHHmSMMTk9W
# qn7H4dLhUBP5YEwbgbN4uv1i9ltTnHli8t1xHV0StX9BFgrnmunTX19kUXY1H5OR
# JbRZyZDdvm1oZyteDj0SnMozr+YSmdIleDUTXdfoY7b2taz8s2+QbOxLxcahEIYG
# Wzqu6h955tKwcANHcZ4gTmAhT3btuOiQsQIDAQABo4IBbjCCAWowHwYDVR0jBBgw
# FoAU9ndq3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFDp0pQxnxkJQwv21/Me7
# KTSC9Hq5MA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMCMGA1UdIAQcMBowCAYGZ4EMAQQCMA4GDCsGAQQBsjEB
# AgEDCDBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLnNlY3RpZ28uY29tL1Nl
# Y3RpZ29QdWJsaWNUaW1lU3RhbXBpbmdSb290UjQ2LmNybDB8BggrBgEFBQcBAQRw
# MG4wRwYIKwYBBQUHMAKGO2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1
# YmxpY1RpbWVTdGFtcGluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8v
# b2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMt5SR2bxngNm+N8o
# c6Gq76Gx1c235fkX7jw8Ho9MAkJGADerHE7dhsBXttqmzgr/7ZZahZSykGRPhPY1
# crj028kB8KzO0dKC2qQBAwtfgqMLKkkX/6bYq2uT33eD6ByAp2/XKD0LcmZh0kKe
# cvSBr6ln9ajX6u1dnx2fA7xEKy1M3qBhfQSUWLtjs2nFt0ELVLptzTlX9ID0cL+i
# OPfdboZ3CelT+JXKVKR2Sge0d4YiFAtPZkfSo8z1Z1x7y/Z9mwMIlBAnyuWXs4Ys
# NuxdrYIt/QxE31PDOJ9DesS4Bc7H9OTORlEV/AvfiF/VepKZpira1MzLYuCw+uoL
# Zn/pkpvd+CvNTS+mEHjBJNa6WK1j8qXFu+jIq+sG9QILHiyB6p/xpHrkJu8zkw39
# 3+VqF9eKlTY2VjRxdycZLrVemZ4Yp3wi33b+W58CllH3HqjmowlZ7SOrgmx8YwYO
# kgrHsXOQHyBp6O4FRb8In0+FzjT7ElGie9V7CfhL3IlVFZ4zjuKsZtH1iU3fGu4z
# /JnOGT6sCb0BbTqe/uhvpFCQBdH5xPGIA/LrbQUXjU2tWJgHhTIqnN/HvHyOHi5t
# M4zP3nhgh2rJ6Kqq2xsHBeNYs/R18xQ8DeIg+c90Eoaeh0YlN1KU8AyYol3K9M+q
# Y5ez8syd/7ZlrRnoVewgH3P1pcswgga5MIIEoaADAgECAhEAmaOACiZVO2Wr3G6E
# prPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVu
# aXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZp
# Y2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y
# ayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjELMAkGA1UE
# BhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UE
# AxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34vuTmflN4
# mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSoMcbvIKck
# 6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+njvMk2xq
# bNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHIohzuC8KN
# qfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRdaCtVD2bSl
# bfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837Q4QOZgYq
# VWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8mFSkcSkAE
# xzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI5XDYO962
# WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5boufUnq1UiY
# PIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplKShBSND36
# E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n858JSqPEC
# AwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10XUwA23uf
# oHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbROg79MA4G
# A1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8EKTAnMCWg
# I6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsGAQUFBwEB
# BGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVtLmNvbTAy
# BggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0bmNhMi5j
# ZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93d3cu
# Y2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCiaU58Q7EP8
# 9DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L94C9LGF3v
# jzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rGDxLUUAsO
# 0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f8pXdd3x2
# mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0CdY9rNLqyA
# 3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B7eMcZNhp
# n8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloMU+vUBfSo
# uCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1tVxTpV2Na
# 3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2DaGgK2W1
# eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNjt2ywzOr+
# tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCrW602NCmv
# O1nm+/80nLy5r0AZvCQxaQ4wggbiMIIEyqADAgECAhEA507yVbBQT/rbpt/3/Iuj
# FTANBgkqhkiG9w0BAQwFADBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMSwwKgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5n
# IENBIFI0MTAeFw0yNjAzMjUwMDAwMDBaFw0zNzA2MjQyMzU5NTlaMHIxCzAJBgNV
# BAYTAkdCMRcwFQYDVQQIEw5HcmVhdGVyIExvbmRvbjEYMBYGA1UEChMPU2VjdGln
# byBMaW1pdGVkMTAwLgYDVQQDEydTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5n
# IFNpZ25lciBSMzcwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCy/8Nt
# S9xQ2UUtBRF32bj7VK3n4m50Uqjk/zTciSziYV40H1LKah0/oEklYG42E4VCP3Dv
# sBUB6DmpCkDZ0jCnZBPIEevaH15ZJOQwFWP2ZXr5YjlJpb68Nlbs+ElNvKx32/1Y
# Hde3qqUSLybjulxPLz6T85+HOIqK7M1Bep8LspyhEP/q6nw5kGxTSrGvufmeH+JF
# 8CnVBcVMFA40FlIYh0cDJVFhhfTfdWgLy/vWuLMQoKkf3s/FvByf16r0rtbyHm/i
# emwxSioJL9zyZDDKUNAbHXl0dhXo2VxUV2NcPXWXuoKsjL+6cfk6Vm2DHnxAlFdF
# saBDIF1JOkSnC6PeLlBznZn2buF3vIIYJcq6N/zeFRCk4/HXDz7zgRsRRMdUB+rh
# yk5FoZaBjw0nLq3GZ3fClLUx5es5pUAxzNODMBn7JkFYip2BAGBPER5eV0ROhk6t
# GTG+fUiMiV+vgjg1YnP5FvnYWyEtWeQD/B2hp3vz0RvtdkM0p3igyadzrfpOBq5p
# pVk/YsuhTQkP99ivneHAGfi5e7lmxJ+meoBPrRLuzMmb81rzzbESjJHMsn5RVtc6
# Ucs7rcMqQC13PUIO7BbGBETV2ufCmV6lPTp3P7XJOvmnUCRTPbVvMTpxP/z+SOHg
# 4/OCBhiqs4FA9+4oQvlkk9w32NGASli9GWrm5wIDAQABo4IBjjCCAYowHwYDVR0j
# BBgwFoAUOnSlDGfGQlDC/bX8x7spNIL0erkwHQYDVR0OBBYEFGEQ6XoSr1HEhdTy
# z6R0D1DNIK/4MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMEoGA1UdIARDMEEwCAYGZ4EMAQQCMDUGDCsGAQQBsjEB
# AgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzBKBgNV
# HR8EQzBBMD+gPaA7hjlodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJs
# aWNUaW1lU3RhbXBpbmdDQVI0MS5jcmwwegYIKwYBBQUHAQEEbjBsMEUGCCsGAQUF
# BzAChjlodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3Rh
# bXBpbmdDQVI0MS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28u
# Y29tMA0GCSqGSIb3DQEBDAUAA4ICAQAD6j2N0azN+hl6k6bKB5/U6VuSOs93ZBb3
# Pczy9VtBIKu4947Z5GwL0aFngIxl+GSuLFrJgPruBCRvKJEJsm7kv+LQ1COVCEG9
# tZ+IRtr4ocUoa53lgdFaENlS0N4wgkZkbQEPv+x+1lSjYh+T4JeL9mUznT7Erc6S
# p5dWLka5sMP/m3GZi6oJPdPcsCKWagH7m2H2xDGIyHJC5PdH9phvi/KmhkktiSVT
# NNqVeV5bWdX2zhRE6UTfz0IcMoCL996lFIydXxOCE4MNDHDM0as4lnTiT/KHMccO
# 6l8c9TnUVgmpci9ar1IABZ2U1XUkYjGGSn9MC3EHDP9V39VuBVvZ33/BEV/EWSRr
# f07T7jFplKX+gQr/UOqPGMlE7ZJ72UaUkNJy7bVl3bcLKzdpjIHzLkf/4MVa1V7w
# 8wqCv5W4gOnRGTlud5UMARbRM8BPxR/CXYXoMmIOD8pmTk2axgRL4LG8XtuchISd
# CHRmtacAmLGq5XSYSVTHTXADlO48iDKh3HM2r98LSF6f0sG12d8V9Jn7C3wDUieO
# xuKj4MdWrW+hiJU2kF87v6eH00HgCFFc2V0+CvfOCMn7juzS41jLaINcBlKWQ/fK
# b/uDLfWOW73z1I2lFY7Xj8tQ1XYtK5eREjWItM8jpl1cbQOc88btR+0XS2TmboE/
# 141+va2PWzGCBcMwggW/AgEBMGowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFz
# c2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUgU2ln
# bmluZyAyMDIxIENBAhAIMk+dt9qRb2Pk8qM8Xl1RMA0GCWCGSAFlAwQCAQUAoIGE
# MBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB
# gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE
# MSIEILw1znFX03T5k+VhhWH1AYsFLQiR+K7f4k5b8KbxFYm6MA0GCSqGSIb3DQEB
# AQUABIIBgCkdLTiNEa9vh0ljLH5L+DkxvpXCplCj1jGgaLMuT7aOULTcxHlUrbdB
# iZH2q12TKUHrS50OT97lcVlliiSRoK1nxH7Gtkp50dX/qZuHhLeX2ArjyX3/1gGl
# tOEkNSsbSDNTedEAxZPXWOKg1FuS80+SvAQYQ6E4NcPRepzwCSP3pbdb5CCPfKJ4
# eji5SYQ0zPxuCUi1Qy+5bgAmLeOmrsPZzymn3TIUh5+xSlfO+gL1hPnea7m+DxP2
# SGts0vG1g2Ir19ypycZ7phd/G6Bqk7IxUjP1i3bm+DV91Xv2e1FgNTjnibG09dlG
# imbe1X9TdfPI1Nob4V1BljAGlnfSAoVw3f9lZ4QWHzfWT0G0CCEXSlcWyzN9CJ2B
# SOfYoCb2876W7G9V/4Qbzb41EoUKWTSSU5Ghp+6crTDmAZthAO2wO6AlL+A3xV9d
# SHSftVNliGHlRZIgG1rh5EzJgash0baDR0I6nJyJM0O7yJytK2AUsjpXVymksBdH
# UNysqS7vnaGCAyMwggMfBgkqhkiG9w0BCQYxggMQMIIDDAIBATBqMFUxCzAJBgNV
# BAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxLDAqBgNVBAMTI1NlY3Rp
# Z28gUHVibGljIFRpbWUgU3RhbXBpbmcgQ0EgUjQxAhEA507yVbBQT/rbpt/3/Iuj
# FTANBglghkgBZQMEAgIFAKB5MBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJ
# KoZIhvcNAQkFMQ8XDTI2MDcwODE5MDY1OFowPwYJKoZIhvcNAQkEMTIEMEE1v0kF
# BAyjZTfiasHWZxNpj+FpDY2nAM8kD1yibMa4VlSRigMxcZTGboBdXyBxkDANBgkq
# hkiG9w0BAQEFAASCAgCVlt0LyeWXrFhVc8kXH0aIN2k2CmSpx+kkHr2LHAbZqyLK
# 87IW234s7IHMHLpVM/2nMWFHlUMZ5TK4W344h8zMeaFNARMDKgeWkHmneCei8nnB
# 6z66P4f3TkYHjiprOrVbTa4GYRlzth0rrCK3/QmSxCnXTqfKoLWAppFADUvetlci
# P41+7HiRPP8ewEC8NGoG7OkxK0oHE6mdOvPBJkh9oi3UuY4j0gNTAR5yHcsXgAfB
# FIZmqrPrjWqogHUl1UbCaqXQQ+YJnvumOvVRqaHNfC+sOfbyJXW/yHbfUouxa+cq
# I6hV2HU4SLy9/RxSGjSFcxwi0vgOvUhaDXQO5Alpaqqew74qw4SPrdTqXbHFmr+6
# CtonYU5ZKndy2t2/Km28hVM/O0qMOOjRhq/NfB8GMBYvihp5xNk+D/aAygp6x3sS
# OEL70ypff+QwZfzGo9TZ4Pyn1/yxJ+oeUGOcZ+M14v4zaXcnlzK5ctjG8JXb2TXj
# rnJ/ox2+rfQi2b6JoztaVjNI+v9l95NdYPQnTF3t0J8/GoXid9rfenmYbRNkeM57
# pt+Vj4q3a4X62E+L+o/hPeHIUWMdgmO/Q3BPds3dkp1va4DOSbbVwiIwCrVY2wE0
# ycD+sDzs24DJyiFNfbGuTtEyYVD/HR/nXY0/oQxKICTPvPdcr32sE1XOwMC3Eg==
# SIG # End signature block