<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MainsKeeper — complete design review</title>
<link rel="icon" href="brand/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="app/assets/tokens.css">
<link rel="stylesheet" href="app/assets/base.css">
<link rel="stylesheet" href="app/assets/components.css">
<script>
/* Theme before first paint — same order as the product shell: ?theme → localStorage["mk-theme"] → OS preference. */
(function () {
  var p = null, s = null, t;
  try { p = new URLSearchParams(location.search).get('theme'); } catch (e) {}
  try { s = localStorage.getItem('mk-theme'); } catch (e) {}
  t = (p === 'dark' || p === 'light') ? p : (s === 'dark' || s === 'light') ? s :
      (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
  document.documentElement.setAttribute('data-theme', t);
})();
</script>
<style>
.rv-skip { position: absolute; left: var(--mk-space-4); top: -80px; z-index: var(--mk-z-skip); background: var(--mk-btn-primary-bg); color: var(--mk-btn-primary-fg); padding: var(--mk-space-2) var(--mk-space-3); border-radius: var(--mk-radius-sm); }
.rv-skip:focus { top: var(--mk-space-3); }
.rv-top { border-bottom: 1px solid var(--mk-border); background: var(--mk-surface-1); }
.rv-top__in { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--mk-space-3); padding-block: var(--mk-space-3); }
.rv-brand { display: inline-flex; align-items: center; gap: var(--mk-space-3); min-width: 0; }
.rv-brand img { height: 28px; width: auto; display: block; }
.rv-logo-light { display: none !important; }
[data-theme="light"] .rv-logo-dark { display: none !important; }
[data-theme="light"] .rv-logo-light { display: block !important; }
.rv-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--mk-space-2); }
.rv-btn { display: inline-flex; align-items: center; gap: var(--mk-space-2); min-height: 44px; padding: 0 var(--mk-space-4); border-radius: var(--mk-radius-md); border: 1px solid var(--mk-border-strong); background: var(--mk-surface-1); color: var(--mk-text); font: inherit; font-size: var(--mk-fs-14); cursor: pointer; text-decoration: none; }
.rv-btn:hover { background: var(--mk-surface-2); }
.rv-btn--primary { background: var(--mk-btn-primary-bg); color: var(--mk-btn-primary-fg); border-color: transparent; font-weight: var(--mk-fw-semibold); }
.rv-btn--primary:hover { background: var(--mk-btn-primary-bg); opacity: .9; }
.rv-intro { padding-block: var(--mk-space-7) var(--mk-space-5); display: grid; gap: var(--mk-space-3); }
.rv-intro h1 { font-size: var(--mk-fs-40); line-height: var(--mk-lh-tight); letter-spacing: var(--mk-tracking-tight); margin: 0; }
.rv-toc { display: flex; flex-wrap: wrap; gap: var(--mk-space-2) var(--mk-space-4); font-size: var(--mk-fs-14); }
.rv-sec { padding-block: var(--mk-space-6); border-top: 1px solid var(--mk-border); display: grid; gap: var(--mk-space-4); scroll-margin-top: var(--mk-space-4); }
.rv-sec > h2 { font-size: var(--mk-fs-30); line-height: var(--mk-lh-tight); margin: 0; }
.rv-sec h3 { font-size: var(--mk-fs-20); line-height: var(--mk-lh-snug); margin: 0; }
.rv-grid { display: grid; gap: var(--mk-space-4); grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); }
.rv-card { background: var(--mk-surface-1); border: 1px solid var(--mk-border); border-radius: var(--mk-radius-lg); padding: var(--mk-space-4); display: grid; gap: var(--mk-space-3); align-content: start; min-width: 0; }
.rv-card--hero { border-color: var(--mk-brand-mint); }
.rv-links { list-style: none; margin: 0; padding: 0; display: grid; }
.rv-links li { border-top: 1px solid var(--mk-border); padding-block: var(--mk-space-2); display: grid; gap: 2px; min-width: 0; }
.rv-links li:first-child { border-top: 0; }
.rv-links a { display: inline-flex; align-items: center; min-height: 32px; overflow-wrap: anywhere; }
.rv-links .rv-why { font-size: var(--mk-fs-14); color: var(--mk-text-muted); }
.rv-states { display: flex; flex-wrap: wrap; gap: var(--mk-space-1) var(--mk-space-3); font-size: var(--mk-fs-14); }
.rv-states a { display: inline-flex; align-items: center; min-height: 32px; }
.rv-page { display: grid; gap: var(--mk-space-1); padding-block: var(--mk-space-3); border-top: 1px solid var(--mk-border); }
.rv-page:first-of-type { border-top: 0; }
.rv-page__name { font-weight: var(--mk-fw-semibold); }
.rv-page__name a { overflow-wrap: anywhere; }
.rv-page__purpose { color: var(--mk-text-muted); font-size: var(--mk-fs-14); }
.rv-steps { margin: 0; padding-left: 1.4em; display: grid; gap: var(--mk-space-2); }
.rv-steps li { padding-left: var(--mk-space-1); }
.rv-steps .rv-why { display: block; font-size: var(--mk-fs-14); color: var(--mk-text-muted); }
.rv-status { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-weight: var(--mk-fw-semibold); }
.rv-status::before { content: ""; width: 10px; height: 10px; border-radius: 50%; flex: none; }
.rv-status--designed { color: var(--mk-ok); }
.rv-status--designed::before { background: var(--mk-ok); }
.rv-status--partial { color: var(--mk-warning); }
.rv-status--partial::before { border: 2px solid var(--mk-warning); box-sizing: border-box; }
.rv-status--not { color: var(--mk-critical); }
.rv-status--not::before { border: 2px dashed var(--mk-critical); box-sizing: border-box; border-radius: 2px; }
.rv-table td, .rv-table th { vertical-align: top; }
.rv-table td:first-child { font-weight: var(--mk-fw-medium); }
.rv-table .rv-where { display: flex; flex-wrap: wrap; gap: 2px var(--mk-space-3); }
.rv-table .rv-where a { overflow-wrap: anywhere; }
.rv-swatches { display: grid; gap: var(--mk-space-2); grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr)); }
.rv-sw { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: var(--mk-space-2); align-items: center; font-size: var(--mk-fs-14); }
.rv-sw i { width: 28px; height: 28px; border-radius: var(--mk-radius-sm); border: 1px solid var(--mk-border); display: block; }
.rv-sw span { display: grid; min-width: 0; }
.rv-sw code { font-size: var(--mk-fs-12); color: var(--mk-text-muted); overflow-wrap: anywhere; }
.rv-dl { display: grid; gap: 0; margin: 0; }
.rv-dl > div { display: grid; grid-template-columns: minmax(0, 34%) minmax(0, 1fr); gap: var(--mk-space-3); padding-block: var(--mk-space-2); border-top: 1px solid var(--mk-border); }
.rv-dl > div:first-child { border-top: 0; }
.rv-dl dt { font-weight: var(--mk-fw-semibold); }
.rv-dl dd { margin: 0; color: var(--mk-text-muted); }
.rv-note { font-size: var(--mk-fs-14); color: var(--mk-text-muted); max-width: 80ch; }
.rv-num { font-family: var(--mk-font-data); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rv-counts { display: grid; gap: var(--mk-space-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr)); }
.rv-count { background: var(--mk-surface-1); border: 1px solid var(--mk-border); border-radius: var(--mk-radius-md); padding: var(--mk-space-3); display: grid; gap: 2px; }
.rv-count b { font-family: var(--mk-font-data); font-size: var(--mk-fs-24); font-weight: var(--mk-fw-medium); font-variant-numeric: tabular-nums; }
.rv-count span { font-size: var(--mk-fs-14); color: var(--mk-text-muted); }
.rv-foot { border-top: 1px solid var(--mk-border); padding-block: var(--mk-space-5); font-size: var(--mk-fs-14); color: var(--mk-text-muted); }
.rv-foot__in { display: flex; flex-wrap: wrap; gap: var(--mk-space-3); justify-content: space-between; }
.rv-dataset { font-size: var(--mk-fs-14); color: var(--mk-text-muted); white-space: nowrap; }
.rv-vis td { font-size: var(--mk-fs-12); color: var(--mk-text-muted); }
.rv-vis td:first-child { font-size: var(--mk-fs-14); color: var(--mk-text); }
.rv-nv { display: block; font-weight: var(--mk-fw-semibold); color: var(--mk-warning); }
@media (max-width: 767px) {
  .rv-intro h1 { font-size: var(--mk-fs-30); }
  .rv-sec > h2 { font-size: var(--mk-fs-24); }
  .rv-dl > div { grid-template-columns: minmax(0, 1fr); gap: var(--mk-space-1); }
  .rv-links a, .rv-states a { min-height: 44px; }
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } }
</style>
</head>
<body>
<a class="rv-skip" href="#main">Skip to content</a>
<header class="rv-top">
  <div class="mk-container rv-top__in">
    <span class="rv-brand">
      <img class="rv-logo-dark" src="brand/lockup-horizontal-dark.svg" alt="MainsKeeper" width="155" height="28">
      <img class="rv-logo-light" src="brand/lockup-horizontal-light.svg" alt="MainsKeeper" width="155" height="28">
      <span class="mk-eyebrow">Design review</span>
    </span>
    <span class="rv-tools">
      <span class="mk-fict-tag">Fictional demonstration data</span>
      <span class="rv-dataset" id="rv-dataset">Dataset: Demo fleet</span>
      <button type="button" class="rv-btn" id="rv-reset">Reset demo</button>
      <button type="button" class="rv-btn" id="rv-theme" aria-pressed="false">Light theme</button>
    </span>
  </div>
</header>

<main id="main" class="mk-container" tabindex="-1">
  <section class="rv-intro" aria-labelledby="rv-h1">
    <p class="mk-eyebrow">Checkpoint 0.1.12 · complete-design review · not final · browser tests skipped by request</p>
    <h1 id="rv-h1">MainsKeeper — complete design review</h1>
    <p class="mk-lead">One product design: a browsable prototype of the web app (dark by default, light by switch) on a shared prototype state, a design system, onboarding and enrollment, network UPS setup, and a proposed CLI contract. Use this page to walk ten journeys, open every page and state, and check what is designed.</p>
    <p class="rv-note"><strong>Read this first:</strong> this 0.1.12 archive is for your manual review. Browser testing was skipped at your request; no browser journeys, visual checks or screenshots were completed. Application code is unchanged from 0.1.11. Its 210 passing state checks and 151 matching CLI exports are retained as historical evidence and were not rerun. See <a href="#c0112">0.1.12 review notes</a>, <a href="#c0111">0.1.11 results</a>, the <a href="../verification/REVIEW.txt">current review record</a> and the <a href="#visual">outstanding visual checklist</a>. “Designed” means implemented in source, not visually verified.</p>
    <nav aria-label="On this page" class="rv-toc">
      <a href="#c0112">0.1.12 review notes</a>
      <a href="#c0111">0.1.11 results</a><a href="#c0110">0.1.10 changes</a><a href="#c019">0.1.9 changes</a><a href="#c018">0.1.8 corrections</a><a href="#corrections">0.1.7 corrections</a><a href="#changed">What changed in 0.1.6</a><a href="#start">Start here</a><a href="#journeys">Journeys</a><a href="#ownership">Configuration ownership</a><a href="#map">Product map</a><a href="#coverage">State coverage</a><a href="#visual">Visual checklist</a><a href="#screenshots">Screenshots</a><a href="#cli-examples">CLI examples</a><a href="#system">Design system</a><a href="#scenario">Scenario</a><a href="#exploration">Preserved exploration</a>
    </nav>
  </section>

  <!-- 0.1.12 manual review checkpoint -->
  <section class="rv-sec" id="c0112" aria-labelledby="h-c0112">
    <p class="mk-eyebrow">Checkpoint 0.1.12 · manual review · not final</p>
    <h2 id="h-c0112">Ready for your review</h2>
    <p class="rv-note">Packaged at your request with browser tests skipped. This patch updates current version labels and review documentation only. The selected Lattice identity, Datum equipment workflows, concepts 1–9, application behaviour and feature scope are preserved from 0.1.11.</p>
    <article class="rv-card" aria-labelledby="c112-evidence">
      <h3 id="c112-evidence">Evidence and limitations</h3>
      <p>No new browser checks, screenshots or defect fixes are claimed. The 0.1.11 numerical and static results below remain historical evidence; they were not rerun for this documentation-only round. The earlier browser launch failure is retained in the <a href="../verification/REVIEW-0.1.11.txt">original 0.1.11 review record</a>.</p>
      <p>Start with the <a href="#journeys">ten journeys</a>. The <a href="#visual">375 / 768 / 1024 / 1440 px checklist in both themes</a> and <a href="#screenshots">screenshot routes</a> remain available for manual review. Visual and interaction acceptance remain outstanding.</p>
    </article>
  </section>

  <!-- 0.1.11 correction and verification -->
  <section class="rv-sec" id="c0111" aria-labelledby="h-c0111">
    <p class="mk-eyebrow">Checkpoint 0.1.11 · correction and verification · not final</p>
    <h2 id="h-c0111">Exact aggregate range boundaries</h2>
    <p class="rv-note">Continues the original 0.1.10 archive in a separate extracted directory. The selected Lattice identity, Datum equipment workflows, concepts 1–9, brand files and feature scope are preserved. This is a patch correction, not an implementation handoff.</p>
    <article class="rv-card" aria-labelledby="c111-fix">
      <h3 id="c111-fix">What changed</h3>
      <p>Each UPS aggregate bucket measures only <code>[max(bucketStart, requestedFrom), min(bucketEnd, requestedTo))</code>. Minute-grid lookup is separate: charge and load statistics include only sample timestamps inside the request. Unknown time, coverage, power shares and pending shares use exact duration intersections.</p>
      <div class="mk-table-wrap" tabindex="0" role="region" aria-label="Verified aggregate range results">
        <table class="mk-table rv-table"><caption>Group 9 scenario B · Node results, not browser screenshots</caption>
          <thead><tr><th scope="col">Requested range</th><th scope="col">Total</th><th scope="col">Unknown</th><th scope="col">Coverage</th></tr></thead>
          <tbody><tr><th scope="row">15:28:30–15:28:40</th><td>10 s</td><td>0 s</td><td>100%</td></tr><tr><th scope="row">15:25:15–15:30:15</th><td>5 min</td><td>4.75 min</td><td>5%</td></tr></tbody>
        </table>
      </div>
      <p class="rv-note">Scenario B gaps remain 15:22:27–15:28:27 and 15:28:42–15:34:12. The strengthened sub-minute regression checks aggregate duration, unknown time and coverage alongside series and availability. Additional checks cover all-unknown sub-minute ranges, both created and fixture UPSs, hourly boundaries, power shares and reloads. Earlier short-gap and closely spaced-gap regressions remain.</p>
      <ul class="rv-links">
        <li><a href="../verification/aggregate-range-results.json">Exact aggregate, series and availability results</a></li>
        <li><a href="../verification/aggregate-ranges.js">Runnable numerical reproduction</a></li>
        <li><a href="../verification/state-checks.txt">210 passed · 0 failed — full state-check log</a></li>
        <li><a href="../verification/state-before-fix.txt">Same strengthened suite against 0.1.10: eight expected failures</a></li>
        <li><a href="../verification/cli-checks.txt">CLI self-check and regeneration: 151 unchanged exports</a></li>
        <li><a href="../verification/source-checks.txt">JavaScript syntax, local file references and static-anchor results</a></li>
        <li><a href="../verification/preservation.txt">Preservation and bounded-change results</a></li>
      </ul>
    </article>
    <article class="rv-card" aria-labelledby="c111-browser">
      <h3 id="c111-browser">Browser review remains blocked</h3>
      <p>The prototype was served locally and the review page returned HTTP 200. Playwright 1.64.0 could not launch an installed browser. Google Chrome exited with SIGABRT; the installed Chromium headless shell exited with SIGTRAP after <code>bootstrap_check_in org.chromium.Chromium.MachPortRendezvousServer: Permission denied (1100)</code>. No page opened, so there are no UI results or screenshots.</p>
      <ul class="rv-links"><li><a href="../verification/browser-capability.txt">Google Chrome launch evidence</a></li><li><a href="../verification/browser-headless-capability.txt">Chromium launch evidence with the permission failure</a></li><li><a href="../verification/REVIEW-0.1.11.txt">Completed checks, commands and remaining blockers</a></li></ul>
      <p class="rv-note">Outstanding: actual clicks and form submissions for all ten journeys, all four widths (375, 768, 1024 and 1440 px) in dark and light themes, keyboard focus, dialogs, navigation, overflow, chart labels and gap rendering, persistence through the browser, browser errors, every requested screenshot and corrected-history screenshots. Node checks do not substitute for these. Final acceptance remains pending.</p>
    </article>
  </section>

  <!-- 0.1.10 changes -->
  <section class="rv-sec" id="c0110" aria-labelledby="h-c0110">
    <p class="mk-eyebrow">Checkpoint 0.1.10 · source-only review checkpoint</p>
    <h2 id="h-c0110">0.1.10 — exact monitoring-gap intervals</h2>
    <p class="rv-note"><strong>Browser review was not performed for this checkpoint.</strong> This environment does not permit opening, rendering or screenshotting the prototype. No browser journey, responsive width, theme, focus, dialog or overflow check has been run and no screenshot exists. Everything below was verified in Node from source.</p>

    <div class="rv-card rv-card--hero" role="note" aria-labelledby="h-c0110-ex">
      <h3 id="h-c0110-ex">What changed and what was verified</h3>
      <ul class="rv-links">
        <li><span><strong>Defect:</strong> in 0.1.9 the series and hourly-aggregate gaps were found on the 1-minute sample grid and matched to the first overlapping recorded stop. A gap shorter than a minute (15:22:27–15:22:57) had no sample inside it, so the chart joined the line across it and availability said “No unknown time”. With two gaps 15 s apart, the second was lost.</span></li>
        <li><span><strong>Fix:</strong> the persisted stop/resume intervals are authoritative. Series, power lanes, <code>history()</code> and aggregate unknown spans list every gap with its exact boundaries. Chart segments split at each boundary and no point is kept inside a gap. A short observed interval between gaps stays observed, with no invented points. Unknown time and coverage are interval intersections with the selected range, in fractional minutes; only the text is rounded (30 s, 18 min). A non-zero gap never shows as “No unknown time”.</span></li>
        <li><span><strong>State checks:</strong> <code>"$OD_NODE_BIN" product/app/assets/state-checks.js</code> → <strong>203 passed · 0 failed</strong>. That is 180 from 0.1.9, with three expectations corrected explicitly (below), plus 23 in the new group 9. Group 9 covers reproductions A and B on the created desk UPS and on the fixture BW Rack UPS on the same collector, clipped ranges, agreement between selectors, later readings, a third cycle and a reload.</span></li>
        <li><span><strong>Corrected expectations (not widened):</strong> Office Desk UPS 15:00–15:22 in witness-down is now 4.2 min observed / 17.8 min unknown (was 5 / 17). Group 8’s two-gap availability is now exactly 11.0 min, not “at least whole minutes − 1”. Group 8’s aggregate check now requires the exact gap total.</span></li>
        <li><span><strong>CLI generator:</strong> <code>product/cli/build-examples.js</code> self-check passed; 151 examples regenerated. <strong>Syntax and links:</strong> every script passes <code>node --check</code>; local links and anchors resolve.</span></li>
        <li><span><strong>Not verified:</strong> browser journeys, 375 / 768 / 1024 / 1440 px in both themes, keyboard focus, dialogs, overflow, chart legibility, browser errors, screenshots. The <a href="#visual">unverified checklist</a> and <a href="#screenshots">screenshot list</a> remain open and block acceptance.</span></li>
      </ul>
    </div>

    <article class="rv-card" aria-labelledby="c110-a">
      <h3 id="c110-a">Route A — immediate off/on (30 s gap)</h3>
      <ol class="rv-steps">
        <li><span>Reset demo. <a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> → NUT <code>192.168.1.20</code>, UPS name <code>deskups</code> → credentials → test → Identify → Use.</span><span class="rv-why">Expect: first readings at 15:22:27.</span></li>
        <li><span><a href="app/collector-config.html?id=bw-pi-01">collector-config.html?id=bw-pi-01</a> → UPS monitoring off → save → “Simulate check-in” → UPS monitoring on → save → “Simulate check-in”.</span><span class="rv-why">Expect: gap 15:22:27–15:22:57, “No records — UPS monitoring disabled”, in the chart (line broken at both ends), power lane, history list and hourly view. 24 h availability: “30 s unknown”, not “No unknown time”. Range 15:22–15:23: coverage 50.0%. BW Rack UPS shows its own gap 15:22:26–15:22:57 (31 s).</span></li>
      </ol>
    </article>

    <article class="rv-card" aria-labelledby="c110-b">
      <h3 id="c110-b">Route B — two gaps 15 s apart</h3>
      <ol class="rv-steps">
        <li><span>As route A step 1, then off → check-in → advance 330 s → on → check-in → advance 15 s → off → check-in → advance 300 s → on → check-in. (The demo clock buttons advance in fixed steps; the 330 s / 15 s / 300 s sequence is the one the state checks drive through <code>MK.store.advanceClock</code>.)</span><span class="rv-why">Expect: two gaps 15:22:27–15:28:27 and 15:28:42–15:34:12 in every view. The 15 s between them is on utility in the power lane with no chart point. 24 h unknown: exactly 11.5 min (“12 min unknown” after display rounding). Range 15:25–15:30: 4.75 min unknown, 0.25 min observed, coverage 5.0%. BW Rack UPS: 15:22:26–15:28:27 and 15:28:41–15:34:12.</span></li>
        <li><span>Record more readings, run another short cycle, reload.</span><span class="rv-why">Expect: the first two gaps keep their boundaries; a third is added; all survive the reload.</span></li>
      </ol>
    </article>
  </section>

  <!-- 0.1.9 changes -->
  <section class="rv-sec" id="c019" aria-labelledby="h-c019">
    <p class="mk-eyebrow">Checkpoint 0.1.9 · source-only review checkpoint</p>
    <h2 id="h-c019">0.1.9 — persistent monitoring gaps</h2>
    <p class="rv-note"><strong>Browser review was not performed for this checkpoint.</strong> This environment does not permit opening, rendering or screenshotting the prototype, so no browser journey, responsive width, theme, focus, dialog or overflow check has been run and no screenshot exists. Everything below was verified in Node from source.</p>

    <div class="rv-card rv-card--hero" role="note" aria-labelledby="h-c019-ex">
      <h3 id="h-c019-ex">What changed and what was verified</h3>
      <ul class="rv-links">
        <li><span><strong>Fix:</strong> every UPS-role-off period keeps its own gap. It starts at the last observation before monitoring stopped (recorded at the check-in that applies the change) and ends at the first observation after it resumed. Later readings never move a closed gap; a second off/on cycle adds a gap and keeps the first; both survive a reload. Series, power lanes, history, hourly aggregates and availability use the same gaps. Previously a later reading stretched the first gap (15:22:27–15:27:57 → …15:33:27) and a second cycle erased it.</span></li>
        <li><span><strong>State checks:</strong> <code>"$OD_NODE_BIN" product/app/assets/state-checks.js</code> → <strong>180 passed · 0 failed</strong> (169 from 0.1.8 unchanged; 11 in group 8, including the existing BW Rack UPS on the same collector).</span></li>
        <li><span><strong>CLI generator:</strong> <code>product/cli/build-examples.js</code> self-check passed; 151 examples regenerated.</span></li>
        <li><span><strong>Syntax and links:</strong> every script passes <code>node --check</code>; local links and anchors in the review, notes and launcher pages resolve.</span></li>
        <li><span><strong>Not verified:</strong> browser journeys, 375 / 768 / 1024 / 1440 px in both themes, keyboard focus, dialogs, overflow, chart legibility, browser errors, screenshots. The <a href="#visual">unverified checklist</a> and <a href="#screenshots">screenshot list</a> remain open and block acceptance.</span></li>
      </ul>
    </div>

    <article class="rv-card" aria-labelledby="c19-gap">
      <h3 id="c19-gap">Route — two off/on cycles (bw-pi-01)</h3>
      <ol class="rv-steps">
        <li><span>Reset demo. <a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> → NUT <code>192.168.1.20</code>, UPS name <code>deskups</code> → credentials → test → Identify → Use.</span><span class="rv-why">Expect: monitoring with first readings at 15:22:27.</span></li>
        <li><span><a href="app/collector-config.html?id=bw-pi-01">collector-config.html?id=bw-pi-01</a> → UPS monitoring off → save → “Simulate check-in” → “Advance demo clock 5 min” → UPS monitoring on → save → “Simulate check-in”.</span><span class="rv-why">Expect: gap 15:22:27–15:27:57 on the UPS’s history, labelled “No records — UPS monitoring disabled”.</span></li>
        <li><span>“Advance demo clock 5 min”, then repeat the off → check-in → advance → on → check-in cycle.</span><span class="rv-why">Expect: the first gap is unchanged at 15:22:27–15:27:57 and a second gap 15:33:27–15:38:57 appears. BW Rack UPS shows the same two periods (ending 15:27:57 and 15:38:57).</span></li>
        <li><span>Reload the page.</span><span class="rv-why">Expect: both gaps unchanged.</span></li>
      </ol>
    </article>
  </section>

  <!-- 0.1.8 corrections -->
  <section class="rv-sec" id="c018" aria-labelledby="h-c018">
    <p class="mk-eyebrow">Round 7 · checkpoint 0.1.8 · source-only correction pass</p>
    <h2 id="h-c018">0.1.8 corrections — routes, steps and expected outcomes</h2>
    <p class="rv-note"><strong>Press Reset demo before each case.</strong> Browser QA was deferred to a separate browser-capable environment: none of the routes below has been opened in a browser for this checkpoint. Expected outcomes come from the shared selectors run in Node.</p>

    <div class="rv-card rv-card--hero" role="note" aria-labelledby="h-c018-ex">
      <h3 id="h-c018-ex">What was exercised (source / Node only)</h3>
      <ul class="rv-links">
        <li><span><strong>State checks:</strong> <code>"$OD_NODE_BIN" product/app/assets/state-checks.js</code> → <strong>169 passed · 0 failed</strong>. The 136 earlier checks are unchanged and still pass; 33 new checks cover group 6 (UPS role on → off → on) and group 7 (deferred activation).</span></li>
        <li><span><strong>CLI generator:</strong> <code>"$OD_NODE_BIN" product/cli/build-examples.js</code> self-check passes and regenerates the same 151 example files.</span></li>
        <li><span><strong>Syntax:</strong> every changed script passes <code>node --check</code>.</span></li>
        <li><span><strong>Not exercised:</strong> browser journeys, layout at 375 / 768 / 1024 / 1440 px, both themes, keyboard focus, dialogs, overflow, chart labels, browser errors, screenshots. The <a href="#visual">unverified checklist</a> and <a href="#screenshots">“Not captured” screenshot list</a> remain open and block acceptance.</span></li>
      </ul>
    </div>

    <div class="rv-grid">
      <article class="rv-card" aria-labelledby="c18-role">
        <h3 id="c18-role">A · UPS role on → off → on (bw-pi-01)</h3>
        <ol class="rv-steps">
          <li><span>Add and use a NUT source: <a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> → NUT <code>192.168.1.20</code>, UPS name <code>deskups</code> → credentials → test → Identify → Use.</span><span class="rv-why">Expect: monitoring, first readings, “On schedule”.</span></li>
          <li><span><a href="app/collector-config.html?id=bw-pi-01">collector-config.html?id=bw-pi-01</a> → turn UPS monitoring off → save → review control “Simulate check-in”.</span><span class="rv-why">Expect: roles Applied (connectivity only). Every UPS read by bw-pi-01 — the new one, BW Rack UPS and BW Office UPS — shows “Monitoring disabled · last known HH:MM”, not current.</span></li>
          <li><span>Review control “Advance demo clock 5 min” (any collector page), then open the UPS, <a href="app/index.html">fleet</a>, <a href="app/sources.html?collector=bw-pi-01">sources</a> and <a href="app/history.html?site=broadwater&amp;ups=bw-rack-ups">history</a>.</span><span class="rv-why">Expect: the last reading time and values do not move; source “Monitoring disabled — UPS role off”; power lane and charts show “No records — UPS monitoring disabled” from the last reading to now; availability counts it as unknown.</span></li>
          <li><span>Turn UPS monitoring back on → save.</span><span class="rv-why">Expect: Pending — still “Monitoring disabled”; nothing resumes before the collector applies it.</span></li>
          <li><span>“Simulate check-in”.</span><span class="rv-why">Expect: applied, then one new reading (“readings resumed: …” in the review output); status current again; the disabled span stays a gap between the last reading and the first new reading.</span></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="c18-def">
        <h3 id="c18-def">B · Deferred activation — SNMP 10.0.20.16 while bw-pi-01 is offline</h3>
        <ol class="rv-steps">
          <li><span><a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> → review control “Simulate disconnect”.</span><span class="rv-why">Expect: bw-pi-01 not reporting.</span></li>
          <li><span>SNMP <code>10.0.20.16</code> → credentials Configured → Test read access.</span><span class="rv-why">Expect: “Test queued — collector not reporting”, never success.</span></li>
          <li><span>Name it, then Save choice.</span><span class="rv-why">Expect: “Configured — waiting for collector”, with the name kept.</span></li>
          <li><span>“Simulate collector reconnect”.</span><span class="rv-why">Expect: queued test succeeds; the UPS is identified (APC Smart-UPS SMT1500 · AP9631 card, serial AS1811240377); monitoring does <strong>not</strong> start — “Identified — confirm possible duplicate”, “Possibly the same UPS as BW Rack UPS (serial match)”. No UPS asset is created and no charge is invented.</span></li>
          <li><span>“It is a separate UPS — start monitoring”.</span><span class="rv-why">Expect: the UPS is created with your saved name and the identified model; readings are the endpoint’s reported values. A value the device does not report shows “Not reported”, never a default 100 %.</span></li>
        </ol>
        <p class="rv-note">Immediate and deferred activation now pass the same prerequisites: successful read test → identification → duplicate reconciliation → confirmation when needed.</p>
      </article>

      <article class="rv-card" aria-labelledby="c18-copy">
        <h3 id="c18-copy">C · Copy and documentation</h3>
        <ul class="rv-links">
          <li><span><strong>ESP32 storage:</strong> <a href="app/collector-config.html?id=shop-esp32-ups">collector-config.html?id=shop-esp32-ups</a> → Local settings read “fixed in firmware … not settings”, badge “Fixed in firmware” (was “change them on the device’s local setup page” / “Set locally”).</span></li>
          <li><span><strong>Grace:</strong> generic wording now says the contact grace is max(5 min, 3 × heartbeat). Specific incident timelines keep “5 min grace”, which is correct for their 30 s heartbeats.</span></li>
          <li><span><strong>Coverage:</strong> 14:30–15:22 = 52 min, 34.2 observed, 17.8 unknown (15:04:12–15:22), coverage 65.8%. 15:00–15:22 (and the 7-day 15:00 hour bucket) = 22 min, 4.2 observed, 17.8 unknown, coverage 19.1%. <em>Corrected in 0.1.10: 0.1.8 quoted minute-sampled values (35 / 17 / 67.3% and 5 / 17 / 22.7%); the exact interval values are above.</em> The 0.1.7 “23 minutes / 18 unknown” sentence and its open question are removed.</span></li>
        </ul>
      </article>
    </div>
  </section>

  <!-- 0.1.7 corrections -->
  <section class="rv-sec" id="corrections" aria-labelledby="h-corr">
    <p class="mk-eyebrow">Round 6 · checkpoint 0.1.7</p>
    <h2 id="h-corr">0.1.7 corrections — routes, steps and expected outcomes</h2>
    <p class="rv-note"><strong>Press Reset demo before each case.</strong> Cases (b) and the home-laptop journey both register the same identity, <code>home-lan|nut|192.168.1.20|3493|rackups</code>. Whichever you run second hits the hard-duplicate block, so reset between them. Cases (a) and (c) use <code>broadwater-lan</code> and do not collide with either. All rules come from shared logic (<a href="app/assets/CONTRACT.md">CONTRACT.md §18</a>); pages only display them.</p>

    <div class="rv-card rv-card--hero" role="note" aria-labelledby="h-corr-ex">
      <h3 id="h-corr-ex">What was exercised (and what was not)</h3>
      <ul class="rv-links">
        <li><span><strong>Shared logic:</strong> <code>"$OD_NODE_BIN" product/app/assets/state-checks.js</code> passed <strong>136/136</strong> at build time and again at integration. It drives <code>MK.store</code> and <code>MK.select</code> in Node with an in-memory localStorage. It covers every case below plus the regressions.</span></li>
        <li><span><strong>Pages (builders report):</strong> Node fake-DOM harness checks of the rendered HTML text for each route below.</span></li>
        <li><span><strong>CLI (builder reports):</strong> <code>cli-screens.js</code> self-check and export generation; 151 files in <code>cli/examples/</code>.</span></li>
        <li><span><strong>Integration:</strong> Node probes of the shared selectors for the expected strings quoted here (home-laptop journey, witness-down availability, freshness per scenario). A link check confirmed every local target in this page, the notes and the launcher exists on disk.</span></li>
        <li><span><strong>Not exercised:</strong> clicks and form-submit wiring in a browser, chart drag, layout, visuals and contrast. Nothing was rendered, previewed or screenshotted. See the <a href="#visual">unverified browser checklist</a>.</span></li>
      </ul>
    </div>

    <div class="rv-grid">
      <article class="rv-card" aria-labelledby="c-id">
        <h3 id="c-id">1 · Network UPS identity (a / b / c) and untrustworthy serials</h3>
        <p class="rv-note">Identity = namespace | protocol | host | port | NUT UPS name. A host alone is never a UPS identity. A trustworthy serial match at a different identity can only raise a question. It never blocks.</p>
        <ol class="rv-steps">
          <li><span><strong>(a) One server, two UPSs.</strong> <a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> → NUT, host <code>192.168.1.20</code>, UPS name <code>rackups</code> → save → Simulate credentials “Configured” → Test read access → Identify UPS → Use.</span><span class="rv-why">Expect: identity <code>broadwater-lan|nut|192.168.1.20|3493|rackups</code>, CyberPower OR1500LCDRM1U, monitoring starts. Then “Add another network UPS” → same host, UPS name <code>deskups</code>: it registers as a second UPS (APC Back-UPS RS 1500).</span></li>
          <li><span><strong>Untrustworthy serial.</strong> The <code>deskups</code> identify step shows serial <code>000000000000</code> flagged untrustworthy.</span><span class="rv-why">Expect: the serial decides nothing. No soft or hard duplicate.</span></li>
          <li><span><strong>Re-adding rackups</strong> on bw-pi-01 (same identity).</span><span class="rv-why">Expect: blocked before any draft, “Already monitored by bw-pi-01 as &lt;the name you gave rackups&gt;”.</span></li>
          <li><span><strong>(b) Same address, other network.</strong> Reset demo. <a href="app/network-source.html?collector=home-studio&amp;state=calm">network-source.html?collector=home-studio&amp;state=calm</a> → NUT <code>192.168.1.20</code> <code>rackups</code>.</span><span class="rv-why">Expect: allowed, key <code>home-lan|nut|192.168.1.20|3493|rackups</code> (Eaton 5P 1550, a different physical UPS). Credential step for home-studio (macOS) shows <code>sudo mainskeeper sources auth …</code> and the macOS Keychain.</span></li>
          <li><span><strong>(c) Hard duplicate.</strong> Reset demo. <a href="app/network-source.html?collector=bw-pi-01">bw-pi-01</a> → SNMP <code>10.0.20.15</code>.</span><span class="rv-why">Expect: blocked before any draft is created: “Already monitored by bw-pi-01 as BW Office UPS”.</span></li>
          <li><span><strong>Soft duplicate.</strong> Same page → SNMP <code>10.0.20.16</code> → credentials → test → Identify UPS.</span><span class="rv-why">Expect: confirm step “Possibly the same UPS as BW Rack UPS (serial match) — confirm” with continue or cancel. Continue → Use creates a separate UPS. Cancel removes the draft and adds nothing.</span></li>
        </ol>
        <p class="rv-note">Deviations: Identify is required before Use. The soft-duplicate confirmation is page-local, so reloading the page asks again. CLI: <a href="cli/examples/sources-add-rackups.80.txt">add rackups</a> · <a href="cli/examples/sources-add-deskups.80.txt">add deskups</a> · <a href="cli/examples/sources-add-duplicate.80.txt">duplicate (exit 1)</a> · <a href="cli/examples/sources-test-serial-match.80.txt">serial match (exit 10)</a>.</p>
      </article>

      <article class="rv-card" aria-labelledby="c-gate">
        <h3 id="c-gate">2 · Collector availability and UPS-role gating — home-laptop journey</h3>
        <ol class="rv-steps">
          <li><a href="app/network-source.html?collector=home-laptop">network-source.html?collector=home-laptop</a><span class="rv-why">home-laptop is a macOS workstation, connectivity only, asleep since 13:05. Add NUT <code>192.168.1.20</code> <code>rackups</code>. Saving works while it sleeps.</span></li>
          <li><span>Simulate credentials “Configured” → Test read access.</span><span class="rv-why">Expect: badge “Test queued — collector not reporting”, with “home-laptop is asleep — test will run when it reports”. Neither failure nor success.</span></li>
          <li><a href="app/collector-config.html?id=home-laptop">collector-config.html?id=home-laptop</a><span class="rv-why">Turn UPS monitoring on and save. Expect: “pending — desired on, applies at the next check-in”.</span></li>
          <li><span>Review control “Simulate collector reconnect”.</span><span class="rv-why">Expect: home-laptop reporting again 15:22:30. The check-in applies the role and the 120 s reporting interval, and the queued test runs: success.</span></li>
          <li><span>Back on network-source (<code>&amp;source=src-home-laptop-nut-192-168-1-20-rackups</code> resumes it) → Identify UPS → Use.</span><span class="rv-why">Expect: first readings 15:22:57 (after the reconnect), reading “On schedule · read N s ago · every 120 s”.</span></li>
          <li><a href="app/collector.html?id=home-laptop">collector.html?id=home-laptop</a><span class="rv-why">Same collector and UPS. “Simulate disconnect (sleep)” → the source reads “Configured — waiting for collector”.</span></li>
          <li><span><strong>Role off variant.</strong> Reset demo. Simulate reconnect first, leave UPS monitoring off, then test → identify → Use.</span><span class="rv-why">Expect: saved as “Configured — waiting for UPS role”, with a link to Configuration. No readings until the role applies.</span></li>
        </ol>
        <p class="rv-note">Account connectivity-only path (onboarding): Reset demo → <a href="app/welcome.html?scenario=empty">welcome.html?scenario=empty</a> → step 2 site “Cabin” (namespace <code>cabin-lan</code>) → step 3 Windows, Connectivity only → approve <a href="app/enroll.html?scenario=empty&amp;enroll=confirm&amp;code=PLHV-GWCS&amp;from=welcome">PLHV-GWCS</a> (den-win-01). Step 7 expects “Enable UPS role later” → configuration. The network branch lists the endpoints reachable in <code>cabin-lan</code>. A network source saved there reads “Configured — waiting for UPS role” until the role applies at a check-in. The macOS studio-mbp equivalent (code WDJB-MJHT) is the store-checked path. CLI: <a href="cli/examples/sources-add-studio.80.txt">add on studio-mbp</a> · <a href="cli/examples/sources-test-offline.80.txt">local test, cloud unreachable</a> · <a href="cli/examples/sources-use-waiting.80.txt">use → waiting for UPS role (exit 0)</a> · <a href="cli/examples/roles-show-pending.80.txt">roles pending</a> · <a href="cli/index.html#g-gating">CLI gating</a>.</p>
        <p class="rv-note">Deviations: there is no queued-test CLI screen, because the CLI runs on the collector. A collector given the UPS role later shows “No USB UPS found”, and there is no re-discover action.</p>
      </article>

      <article class="rv-card" aria-labelledby="c-wd">
        <h3 id="c-wd">3 · Witness down — no data after 15:04:12</h3>
        <ol class="rv-steps">
          <li><a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22">history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22</a><span class="rv-why">Expect: power lane on utility to 15:04:12, then “No records — collector not reporting” to 15:22. Charge and load end at 15:04. Coverage 65.8% (17.8 min unknown, shown as “18 min unknown”). The site reachability badge is separate, and Office Desk UPS power is never derived from home-esp32-garage.</span></li>
          <li><a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;range=7d">…&amp;range=7d</a><span class="rv-why">Expect: the 15:00 hour bucket covers the 22 minutes elapsed by 15:22 — 4.2 min observed on utility (15:00–15:04:12) and 17.8 min unknown, labelled “No records — collector not reporting”; coverage 19.1%. (The same 4.2 of 22 / 17.8 unknown applies to a 15:00–15:22 range; the 14:30–15:22 range above is 52 min with 17.8 unknown.)</span></li>
          <li><a href="app/ups.html?id=office-desk-ups&amp;scenario=witness-down">ups.html?id=office-desk-ups&amp;scenario=witness-down</a><span class="rv-why">Expect: “Stale · last known 15:04 · read 17 min ago · every 30 s”. 24 h availability counts the gap as unknown, not as up or down.</span></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="c-range">
        <h3 id="c-range">4 · History range 15:00–15:22 (point / interval rule)</h3>
        <ol class="rv-steps">
          <li><a href="app/history.html?site=duplex&amp;from=15:00&amp;to=15:22">history.html?site=duplex&amp;from=15:00&amp;to=15:22</a><span class="rv-why">Expect: no yesterday episode (INC-0409) and no 14:03–14:30 episode. The replay receipt event (e-dx-replay) appears at 15:17:40, its receive time.</span></li>
          <li><a href="app/history.html?from=15:00&amp;to=15:22">history.html?from=15:00&amp;to=15:22</a><span class="rv-why">Expect: only the ongoing BW Rack episode (ep-bw-current). No Home 06:42 event.</span></li>
        </ol>
        <p class="rv-note">Rule: a point is included if from ≤ time ≤ to. An interval is included if start ≤ to and end ≥ from. Its end is the end time, else the last record, else now only while ongoing. Deviations: receipt events sit at their receive time. The default availability period changed. “View this episode” pads the range by 25 min.</p>
      </article>

      <article class="rv-card" aria-labelledby="c-unk">
        <h3 id="c-unk">5 · Unknown evidence propagates everywhere</h3>
        <ul class="rv-links">
          <li><span><strong>Lanes and charts:</strong> <a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22">witness-down history</a>: hatched “No records — collector not reporting”. It is never filled or joined.</span></li>
          <li><span><strong>Aggregates:</strong> <a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;range=7d">7 d</a>: the hour bucket carries the unknown label and unknown minutes.</span></li>
          <li><span><strong>Coverage and availability:</strong> <a href="app/ups.html?id=office-desk-ups&amp;scenario=witness-down">Office Desk UPS</a>: “18 min unknown — counted as unknown, not up or down” for 14:30–15:22 (17.8 min exactly; 0.1.10 corrects the earlier “17 min”).</span></li>
          <li><span><strong>Awaiting the next report:</strong> <a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">BW Office UPS 24 h</a>: after its last 600 s report the series shows “Not reported yet — next report due by 15:25:00 (every 600 s)” rather than a filled value.</span></li>
        </ul>
      </article>

      <article class="rv-card" aria-labelledby="c-fresh">
        <h3 id="c-fresh">6 · Freshness by cadence (30 s vs 600 s)</h3>
        <p class="rv-note">I = reporting interval, g = max(30 s, I/2). On schedule ≤ I + g · Overdue ≤ 2I + g · Stale · last known beyond that, or when contact is lost. 30 s → ≤ 60 s / ≤ 90 s / &gt; 90 s. 600 s → ≤ 15 min / ≤ 25 min / &gt; 25 min. Heartbeat is judged separately.</p>
        <ul class="rv-links">
          <li><span><strong>BW Office UPS (600 s, read 420 s ago):</strong> “On schedule · read 7 min ago · every 10 min” on <a href="app/index.html">fleet</a> · <a href="app/site.html?id=broadwater">site</a> · <a href="app/equipment.html">equipment</a> · <a href="app/ups.html?id=bw-office-ups">ups.html?id=bw-office-ups</a>. It was wrongly shown as Stale before.</span></li>
          <li><span><strong>30 s source, stale:</strong> <a href="app/ups.html?id=shop-bench-ups">Shop Bench UPS</a>: “Stale · last known 15:10 · read 11 min ago · every 30 s”.</span></li>
          <li><span><strong>Legend with worked numbers:</strong> <a href="app/components.html">component gallery</a> · <a href="app/collector.html?id=bw-pi-01">collector detail</a> (heartbeat 15 s override, judged separately).</span></li>
          <li><span><strong>CLI:</strong> <a href="cli/examples/status-bw.80.txt">status-bw</a> (“On schedule · 7 min · every 10 min”) · <a href="cli/examples/status-bw-pi-01.json">JSON</a> (<code>"freshness": {"state":"on-schedule","ageSeconds":420,"expectedIntervalSeconds":600,"allowanceSeconds":300}</code>).</span></li>
        </ul>
        <p class="rv-note">No fixture UPS is currently Overdue. Use the legend to check that wording.</p>
      </article>

      <article class="rv-card" aria-labelledby="c-plat">
        <h3 id="c-plat">7 · Platform-specific instructions</h3>
        <ul class="rv-links">
          <li><span><strong>Windows:</strong> <a href="app/install.html?platform=windows">install.html?platform=windows</a> · <a href="app/enroll.html?enroll=confirm&amp;code=PLHV-GWCS">enroll PLHV-GWCS</a></span><span class="rv-why">Expect: Administrator PowerShell (<code>PS&gt;</code>), no sudo; <code>C:\ProgramData\MainsKeeper\collector.toml</code>; DPAPI; Event Viewer and <code>C:\ProgramData\MainsKeeper\logs</code>. CLI: <a href="cli/examples/config-set-windows.80.txt">config set (elevated)</a> · <a href="cli/examples/config-set-windows-unelevated.80.txt">unelevated (exit 5)</a> · <a href="cli/examples/config-validate-windows.80.txt">validate</a> · <a href="cli/examples/logs-windows.80.txt">logs</a> · <a href="cli/examples/service-windows.80.txt">service</a></span></li>
          <li><span><strong>ESP32:</strong> <a href="app/install.html?platform=esp32">install.html?platform=esp32</a> · <a href="app/enroll.html?enroll=confirm&amp;code=MZQD-TCVF">enroll MZQD-TCVF</a></span><span class="rv-why">Expect: no host command line; the probe’s local setup page (setup access point or <code>http://&lt;device&gt;.local</code>); secrets on the device. CLI: <a href="cli/examples/help-esp32.80.txt">help esp32 (exit 2)</a></span></li>
          <li><span><strong>macOS:</strong> <a href="app/install.html?platform=macos">install.html?platform=macos</a></span><span class="rv-why">Expect: <code>sudo</code>; <code>/Library/Application Support/MainsKeeper/collector.toml</code>; macOS Keychain item “MainsKeeper source &lt;id&gt;”; <code>/Library/Logs/MainsKeeper/</code>. CLI: <a href="cli/examples/sources-local-access-macos.80.txt">local access</a> · <a href="cli/examples/config-validate-macos.80.txt">validate</a> · <a href="cli/examples/logs-macos.80.txt">logs</a></span></li>
          <li><span><strong>Raspberry Pi / Fedora:</strong> <a href="app/install.html?platform=pi">install.html?platform=pi</a> · <a href="app/install.html?platform=fedora">fedora</a> · <a href="app/enroll.html?enroll=confirm&amp;code=TJNR-FBQZ">enroll TJNR-FBQZ (Fedora)</a></span><span class="rv-why">Expect: <code>sudo</code>; <code>/etc/mainskeeper/collector.toml</code>; <code>/etc/mainskeeper/secrets.d/</code>; <code>journalctl -u mainskeeperd</code>. CLI: <a href="cli/examples/sources-local-access-fedora.80.txt">local access (Fedora)</a></span></li>
          <li><span><strong>Unknown platform:</strong> <a href="app/install.html?platform=bogus">install.html?platform=bogus</a> → not-found state. <strong>Re-enroll:</strong> <a href="app/enroll.html?enroll=reenroll&amp;code=KQPT-RNXV&amp;scenario=revoked">KQPT-RNXV</a> → <code>sudo mainskeeper enroll --reenroll</code>.</span></li>
          <li><span><a href="cli/index.html#platforms">CLI · Platforms section</a> · <a href="cli/index.html#elevation">Elevation</a></span></li>
        </ul>
        <p class="rv-note">Deviation: OS install lines (package names and installers) are page-local; every command, path, secret store and log location comes from <code>MK.cli.platform</code>.</p>
      </article>

      <article class="rv-card" aria-labelledby="c-reg">
        <h3 id="c-reg">Regression routes (should behave as in 0.1.6)</h3>
        <ul class="rv-links">
          <li><span><strong>First-run persistence:</strong> walk <a href="#j1">journey 1</a>, then reload <a href="app/index.html">the fleet</a>, <a href="app/collector.html?id=garage-pi-02">garage-pi-02</a> and <a href="app/ups.html?id=cyberpower-cp1500pfclcd">the CyberPower UPS</a>. They are still there. First readings appear only after the collector reports with the UPS role applied.</span></li>
          <li><span><strong>Acknowledgment propagation:</strong> <a href="app/incident.html?id=INC-0412&amp;step=ack">acknowledge INC-0412</a> → the same “jp 15:22:00” acknowledgment on <a href="app/incidents.html">incidents</a>, <a href="app/index.html">fleet</a>, <a href="app/equipment.html">equipment</a> and <a href="app/ups.html?id=bw-rack-ups">UPS</a>.</span></li>
          <li><span><strong>Restoration:</strong> <a href="app/incident.html?id=INC-0412&amp;step=restored">incident.html?id=INC-0412&amp;step=restored</a> (restored 15:31:00, still open) → <a href="app/incident.html?id=INC-0412&amp;step=resolved">step=resolved</a> (15:36:00).</span></li>
          <li><span><strong>Not-found routes:</strong> <a href="app/site.html?id=nope">site</a> · <a href="app/ups.html?id=garage">UPS</a> · <a href="app/history.html?site=nope">history</a> · <a href="app/network-source.html?collector=nope">network source</a>.</span></li>
          <li><span><strong>CLI exports:</strong> <a href="#cli-examples">151 files</a> (121 in 0.1.6 + 30 new). All are in the checkpoint, and their names are credential-guard-safe (checked by state-checks.js).</span></li>
        </ul>
      </article>
    </div>
  </section>

  <!-- What changed -->
  <section class="rv-sec" id="changed" aria-labelledby="h-changed">
    <p class="mk-eyebrow">Previous round · 0.1.6 (corrected where 0.1.7 changed it)</p>
    <h2 id="h-changed">What changed in 0.1.6</h2>
    <div class="rv-grid">
      <article class="rv-card">
        <h3>One shared prototype state</h3>
        <ul class="rv-links">
          <li><span>Every page reads the fixture merged with a shared store (this browser’s <code>localStorage</code>, key <code>mk-proto-v1</code>). Other open tabs update too.</span></li>
          <li><span>Acknowledgments, the INC-0412 restoration, created sites, collectors and UPSs, network sources and configuration changes are the same on every page.</span></li>
          <li><span><strong>Reset demo</strong> in each page’s review bar (and in this header) returns to the pristine fixture.</span></li>
          <li><span><strong>Dataset switch:</strong> Demo fleet | Your account (first run).</span></li>
          <li><span>Unknown ids show a “not found” state instead of another entity.</span></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Journeys completed</h3>
        <ul class="rv-links">
          <li><span>First run ends in your own fleet. The new collector and UPS open on their own pages.</span></li>
          <li><span>New network UPS setup (NUT or SNMP): success, unreachable, rejected credentials, unsupported capability, already monitored (identity-based in 0.1.7), plus the 0.1.7 serial-match confirm and queued test.</span></li>
          <li><span>Re-enrollment uses <code>mainskeeper enroll --reenroll</code>.</span></li>
          <li><span>Witness-down scenario and a sleeping workstation (home-laptop).</span></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Corrected semantics</h3>
        <ul class="rv-links">
          <li><span>History filters apply to charts, episodes and events. Other UPSs’ episodes appear under “Site context”.</span></li>
          <li><span>7 d draws hourly aggregates with coverage, not 24 h.</span></li>
          <li><span>On battery notifies after 60 s. Home’s 38 s episode is “Below 60 s delay — event only”.</span></li>
          <li><span>Six timestamp labels: First evidence · Grace period started · Declared · Acknowledged · Recovered · Resolved.</span></li>
          <li><span>Exact backfill policy and replay definition (&gt; 2 min late while reconnecting).</span></li>
          <li><span>Freshness (0.1.7): relative to each source’s reporting interval: On schedule · Overdue · Stale · last known. See <a href="#c-fresh">correction 6</a>.</span></li>
          <li><span>ESP32 values are labelled “Illustrative — validated board/UPS list pending”.</span></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>One configuration and CLI contract</h3>
        <ul class="rv-links">
          <li><span>One configuration-ownership table for web and CLI: <a href="#ownership">below</a>.</span></li>
          <li><span>Secrets stay on the collector. The dashboard shows only Configured / Missing / Rejected.</span></li>
          <li><span>One CLI grammar with elevation per platform. <code>--replace</code>, <code>scan</code> and <code>select</code> are gone.</span></li>
          <li><span>CLI exports renamed and complete: 121 example files in 0.1.6, now 151 in 0.1.7, all in the checkpoint.</span></li>
        </ul>
      </article>
    </div>
  </section>

  <!-- a. Start here -->
  <section class="rv-sec" id="start" aria-labelledby="h-start">
    <p class="mk-eyebrow">a · Start here</p>
    <h2 id="h-start">Start here</h2>
    <div class="rv-grid">
      <article class="rv-card rv-card--hero">
        <h3>Open the product</h3>
        <p>The fleet overview is the landing page. The demo fleet opens on the incident fixture (Wed 30 Sep 2026, 15:22 PDT).</p>
        <p><a class="rv-btn rv-btn--primary" href="app/index.html">Open MainsKeeper (fleet overview)</a></p>
        <ul class="rv-links">
          <li><a href="app/index.html?theme=dark">Fleet overview — dark theme (default)</a></li>
          <li><a href="app/index.html?theme=light">Fleet overview — light theme</a></li>
          <li><a href="app/index.html?state=calm">Fleet overview — calm state, no incidents</a></li>
          <li><a href="app/index.html?scenario=witness-down">Fleet overview — witness-down scenario</a></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Reset demo</h3>
        <p class="rv-note">The prototype remembers what you do in this browser: acknowledgments, the INC-0412 walkthrough step, configuration changes, network sources and the first-run account. Opening some review links changes this state too. For example, <code>incident.html?id=INC-0412&amp;step=ack</code> acknowledges INC-0412.</p>
        <p class="rv-note"><strong>Reset demo</strong> is in the review bar at the top of every product page and in this page’s header. It asks you to confirm, clears the store and returns to the pristine demo fleet. Reset before each journey to get the results described below.</p>
      </article>
      <article class="rv-card">
        <h3>Dataset switch</h3>
        <p class="rv-note">The review bar shows <strong>Dataset: Demo fleet | Your account (first run)</strong>.</p>
        <ul class="rv-links">
          <li><span><strong>Demo fleet</strong>: Broadwater, Home, Shop and Duplex, in Incident or Calm state, plus the scenarios.</span></li>
          <li><span><strong>Your account (first run)</strong>: starts empty (<code>?scenario=empty</code>). It becomes active when you create a site in onboarding. Pages without <code>state</code>/<code>scenario</code> then show your account until you switch back or reset.</span></li>
          <li><span>Choosing a demo state or scenario switches back to the demo fleet.</span></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Link parameters</h3>
        <p class="rv-note">Links carry only <code>state</code>, <code>scenario</code>, <code>theme</code> and <code>motion</code>. Entity parameters (<code>id</code>, <code>site</code>, <code>collector</code>, <code>ups</code>, <code>focus</code>) and page states (<code>auth</code>, <code>enroll</code>) are never carried to the next page. An unknown id shows “not found”:</p>
        <ul class="rv-links">
          <li><a href="app/site.html?id=nope">site.html?id=nope</a></li>
          <li><a href="app/ups.html?id=garage">ups.html?id=garage</a></li>
          <li><a href="app/incident.html?id=NOPE">incident.html?id=NOPE</a></li>
          <li><a href="app/collector.html?id=nope">collector.html?id=nope</a></li>
          <li><a href="app/sources.html?collector=nope">sources.html?collector=nope</a></li>
          <li><a href="app/sync.html?collector=nope">sync.html?collector=nope</a></li>
          <li><a href="app/relationships.html?site=nope">relationships.html?site=nope</a></li>
          <li><a href="app/relationships.html?focus=ups:nope">relationships.html?focus=ups:nope</a> (message, nothing preselected)</li>
          <li><a href="app/network-source.html?collector=nope">network-source.html?collector=nope</a></li>
          <li><a href="app/enroll.html?enroll=confirm&amp;code=ZZZZ-ZZZZ">enroll.html · unknown code ZZZZ-ZZZZ</a></li>
          <li><a href="app/enroll.html?enroll=approved&amp;device=nope-01">enroll.html · unknown device nope-01</a></li>
          <li><a href="app/install.html?platform=bogus">install.html?platform=bogus</a></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Design system, CLI and brand</h3>
        <ul class="rv-links">
          <li><a href="app/components.html">Component gallery — both themes side by side</a><span class="rv-why">Badges, freshness, staleness legend, incident timestamps, sync meter, charts, tables, inspector, forms</span></li>
          <li><a href="app/components.html?view=dark">Component gallery — dark only</a> · <a href="app/components.html?view=light">light only</a></li>
          <li><a href="app/assets/CONTRACT.md">Shared file contract</a><span class="rv-why">Tokens, components, fixture, store (§12), configuration ownership (§13), CLI grammar (§14), policies (§15), 7 d aggregation (§16), round-6 corrections: identity, gating, history ranges, freshness, platforms, state-checks (§18)</span></li>
          <li><a href="cli/index.html">Proposed <code>mainskeeper</code> CLI contract</a><span class="rv-why"><a href="cli/index.html#elevation">Elevation</a> · <a href="cli/index.html#ownership">Configuration ownership</a> · <a href="cli/index.html#g-network">Network UPS</a> · <a href="cli/index.html#exit">Exit codes</a></span></li>
          <li><a href="brand/README.md">Brand folder README — marks, lockups, icons, clear space, minimum sizes</a></li>
          <li><a href="notes.html">Design notes — provenance, decisions, limitations, open questions</a></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Brand files</h3>
        <ul class="rv-links">
          <li><a href="brand/lockup-horizontal-dark.svg">Horizontal lockup — dark ground</a></li>
          <li><a href="brand/lockup-horizontal-light.svg">Horizontal lockup — light ground</a></li>
          <li><span><a href="brand/logomark-dark.svg">Logomark — dark</a> · <a href="brand/logomark-light.svg">light</a> · <a href="brand/logomark-mono.svg">mono</a></span></li>
          <li><span><a href="brand/app-icon.svg">App icon — dark tile</a> · <a href="brand/app-icon-light.svg">light tile</a></span></li>
          <li><span><a href="brand/favicon.svg">Favicon 16</a> · <a href="brand/favicon-32.svg">32</a> · <a href="brand/mask-icon.svg">Safari mask icon</a></span></li>
        </ul>
      </article>
    </div>
  </section>

  <!-- b. Journeys -->
  <section class="rv-sec" id="journeys" aria-labelledby="h-j">
    <p class="mk-eyebrow">b · Journeys</p>
    <h2 id="h-j">Required journeys, step by step</h2>
    <p class="rv-note">Reset demo before each journey. Steps that change the shared state are marked “changes state”. The pages after them reflect the change.</p>
    <div class="rv-grid">
      <article class="rv-card" aria-labelledby="j1">
        <h3 id="j1">1 · Empty account → site → install → approval → roles &amp; sources → first readings → fleet → same collector and UPS</h3>
        <ol class="rv-steps">
          <li><a href="app/sign-in.html?auth=signed-out&amp;scenario=empty">Sign in with GitHub (Cloudflare Access)</a></li>
          <li><a href="app/sign-in.html?auth=callback&amp;scenario=empty">Callback</a><span class="rv-why">An empty account continues to the welcome flow</span></li>
          <li><a href="app/index.html?scenario=empty">Empty fleet</a><span class="rv-why">No sites, collectors or UPSs; points to setup</span></li>
          <li><a href="app/welcome.html?scenario=empty">Welcome · step 1</a><span class="rv-why">Progress is kept in the store; <code>?step=</code> only chooses the step shown. Walk the steps in order.</span></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=2">Step 2 · create the site</a><span class="rv-why">Name “Home”, time zone America/Los_Angeles. Changes state: the dataset switches to “Your account (first run)”.</span></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=3">Step 3 · choose the collector</a><span class="rv-why">Raspberry Pi · both roles (UPS and Connectivity) · site witness</span></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=4">Step 4 · install</a><span class="rv-why"><code>sudo apt install ./mainskeeper_0.4.2_arm64.deb</code> · detail: <a href="app/install.html?platform=pi">install.html?platform=pi</a></span></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=5">Step 5 · start enrollment on the device</a><span class="rv-why"><code>sudo mainskeeper enroll --headless</code> shows code BKTN-XRWP</span></li>
          <li><a href="app/enroll.html?scenario=empty&amp;enroll=confirm&amp;code=BKTN-XRWP&amp;from=welcome">Step 6 · confirm BKTN-XRWP in the browser</a><span class="rv-why">Approve changes state: creates collector garage-pi-02 and its discovered USB source</span></li>
          <li><a href="app/enroll.html?scenario=empty&amp;enroll=approved&amp;code=BKTN-XRWP&amp;from=welcome">Approved</a></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=7">Step 7 · roles and sources</a><span class="rv-why">USB branch: “Use this UPS” on the CyberPower CP1500PFCLCD (changes state). Network branch: <a href="app/network-source.html?scenario=empty&amp;collector=garage-pi-02&amp;from=welcome&amp;return=welcome.html%3Fstep%3D8">network-source.html?collector=garage-pi-02&amp;from=welcome</a> (returns to step 8)</span></li>
          <li><a href="app/welcome.html?scenario=empty&amp;step=8">Step 8 · first readings</a><span class="rv-why">100% · est. 38 min · load 18% · 121 V · temperature Unsupported; reading “On schedule · read N s ago · every 30 s” (only after garage-pi-02 reports with the UPS role applied)</span></li>
          <li><a href="app/index.html">Go to fleet · your account</a><span class="rv-why">No state/scenario parameter, so the account dataset shows: “1 of 1 expected collectors reporting”</span></li>
          <li><a href="app/collector.html?id=garage-pi-02">Same collector · garage-pi-02</a></li>
          <li><a href="app/ups.html?id=cyberpower-cp1500pfclcd">Same UPS · CyberPower CP1500PFCLCD</a><span class="rv-why">Also: <a href="app/equipment.html?cond=waiting">equipment.html?cond=waiting</a> (before the first reading) · <a href="app/sources.html?collector=garage-pi-02">sources</a> · <a href="app/collector-config.html?id=garage-pi-02">configuration</a></span></li>
        </ol>
        <p class="rv-note">One enrollment code per platform per run. Use Reset demo to enroll the same device again.</p>
      </article>

      <article class="rv-card" aria-labelledby="j2">
        <h3 id="j2">2 · On-battery alert → investigation → acknowledgment → utility restoration</h3>
        <ol class="rv-steps">
          <li><a href="app/index.html">Fleet: Broadwater 1 of 2 UPSs on battery</a></li>
          <li><a href="app/incident.html?id=INC-0412&amp;step=alert">INC-0412 · alert</a><span class="rv-why">First evidence 15:09:00 · Grace period started 15:09:00 · Declared 15:10:00 · email 15:10:04 · push 15:10:05</span></li>
          <li><a href="app/incident.html?id=INC-0412&amp;step=investigate#evidence">Investigate the evidence</a></li>
          <li><a href="app/ups.html?id=bw-rack-ups">BW Rack UPS detail</a><span class="rv-why">UPS estimate vs observed duration; freshness legend</span></li>
          <li><a href="app/incident.html?id=INC-0412&amp;step=ack">Acknowledge (changes state)</a><span class="rv-why">jp 15:22:00 “Utility outage on circuit A — checking with the building”; the incident stays open</span></li>
          <li><a href="app/incidents.html">Incidents list shows the same acknowledgment</a></li>
          <li><a href="app/incident.html?id=INC-0412&amp;step=restored">Utility restored (changes state)</a><span class="rv-why">Clock 15:32 · restored 15:31:00 · 22 min observed on battery · recovered, still open until stable</span></li>
          <li><a href="app/incident.html?id=INC-0412&amp;step=resolved">Resolved (changes state)</a><span class="rv-why">Clock 15:37 · resolved 15:36:00 after 5 min stable</span></li>
          <li><span>Same state everywhere: <a href="app/index.html">fleet</a> · <a href="app/site.html?id=broadwater">site</a> · <a href="app/equipment.html">equipment</a> · <a href="app/ups.html?id=bw-rack-ups">UPS</a> · <a href="app/incidents.html?tab=recovered">incidents</a> · <a href="app/history.html?site=broadwater&amp;ups=bw-rack-ups&amp;range=24h">history</a> · <a href="app/notifications.html">notifications</a></span></li>
        </ol>
        <p class="rv-note">Low runtime: <a href="app/alert-rules.html?rule=rule-low-runtime">rule below 10 min</a>. No fixture UPS crosses it. The round-4 <code>?runtime=low</code> demo was removed.</p>
      </article>

      <article class="rv-card" aria-labelledby="j3">
        <h3 id="j3">3 · Site contact lost → offline recording → recovery → historical backfill (Duplex)</h3>
        <ol class="rv-steps">
          <li><a href="app/incident.html?id=INC-0411">INC-0411 timestamps</a><span class="rv-why">First evidence 13:41:00 (local probe, received 15:02:06) · Grace period started 13:44:00 · Declared 13:49:00 · Acknowledged jp 13:52 · Recovered 15:02:06 · Resolved 15:07:06</span></li>
          <li><a href="app/site.html?id=duplex">Duplex site · local probe evidence</a></li>
          <li><a href="app/sync.html?collector=duplex-pi-probe">duplex-pi-probe buffer</a><span class="rv-why">Journey steps: <a href="app/sync.html?step=1#journey">1</a> · <a href="app/sync.html?step=2#journey">2</a> · <a href="app/sync.html?step=3#journey">3</a> · <a href="app/sync.html?step=4#journey">4</a> · <a href="app/sync.html?case=offline">offline · recording</a> · <a href="app/sync.html?case=replay-live">replay + live</a></span></li>
          <li><a href="app/collector.html?id=duplex-pi-probe">duplex-pi-probe · backlog draining</a></li>
          <li><a href="app/history.html?site=duplex&amp;from=13:30&amp;to=15:30">History 13:30–15:30</a><span class="rv-why">Replay tagging (&gt; 2 min late while reconnecting), the 14:30–14:52 gap, and the backfill alerting note</span></li>
          <li><a href="app/history.html?site=duplex&amp;ep=ep-duplex-today">Episode: at least 27 minutes on battery; endpoint unknown</a></li>
        </ol>
        <p class="rv-note">Backfill policy: “Historical backfill updates history at observation time and does not generate duplicate or obsolete notifications. Fresh evidence of an active condition remains alertable while a backlog drains. Ordinary transmission latency does not make a reading historical replay.”</p>
      </article>

      <article class="rv-card" aria-labelledby="j4">
        <h3 id="j4">4 · Collector online while its UPS source stops reporting</h3>
        <ol class="rv-steps">
          <li><a href="app/index.html?scenario=ups-comms-lost">Fleet · UPS comms lost scenario</a></li>
          <li><a href="app/ups.html?id=garage-ups&amp;scenario=ups-comms-lost">Garage UPS · UPS communication lost</a></li>
          <li><a href="app/collector.html?id=home-esp32-garage&amp;scenario=ups-comms-lost">home-esp32-garage · still reporting</a></li>
          <li><a href="app/incident.html?id=INC-0414&amp;scenario=ups-comms-lost">INC-0414 · warning after 2 min</a></li>
          <li><a href="app/sources.html?case=comms-failing&amp;collector=home-esp32-garage&amp;scenario=ups-comms-lost">Source diagnostics · read timeout</a></li>
          <li><a href="app/equipment.html?cond=comms-lost&amp;scenario=ups-comms-lost">Equipment filtered to UPS comms lost</a></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="j5">
        <h3 id="j5">5 · Credential revoked → diagnosis → re-enrollment</h3>
        <ol class="rv-steps">
          <li><a href="app/index.html?scenario=revoked">Fleet at 15:32 · Broadwater last known</a></li>
          <li><a href="app/collector.html?id=bw-pi-01&amp;scenario=revoked">bw-pi-01 · revoked, uploads rejected</a></li>
          <li><a href="app/collector-config.html?id=bw-pi-01&amp;scenario=revoked">Configuration while revoked (changes stay pending)</a></li>
          <li><a href="cli/index.html#g-doctor">On the device: <code>mainskeeper doctor</code></a><span class="rv-why"><a href="cli/examples/doctor-revoked.80.txt">doctor-revoked.80.txt</a> · <a href="cli/examples/error-enrollment-revoked.json">error-enrollment-revoked.json</a></span></li>
          <li><a href="app/enroll.html?enroll=revoked&amp;scenario=revoked">Enrollment page · revoked</a></li>
          <li><a href="app/enroll.html?enroll=reenroll&amp;code=KQPT-RNXV&amp;scenario=revoked">Re-enroll with <code>sudo mainskeeper enroll --reenroll</code> · code KQPT-RNXV</a><span class="rv-why">Approve changes state · <a href="cli/examples/enroll-reenroll.80.txt">enroll-reenroll.80.txt</a></span></li>
          <li><a href="app/enroll.html?enroll=approved&amp;code=KQPT-RNXV&amp;scenario=revoked">Re-enrollment approved</a></li>
          <li><a href="app/collector.html?id=bw-pi-01&amp;scenario=revoked">bw-pi-01 reporting again (same scenario, after approval)</a></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="j6">
        <h3 id="j6">6 · Network UPS setup — outcomes, identity and gating</h3>
        <p class="rv-note">Open <a href="app/network-source.html?collector=bw-pi-01">network-source.html?collector=bw-pi-01</a> (demo fleet). Steps: Protocol → Endpoint → Local credentials → Test → Identify → Duplicate check → Use. Credentials must show <strong>Configured</strong> before the test. The browser never takes a secret. You run the platform’s credential command on the collector (for example <code>sudo mainskeeper sources auth &lt;source&gt;</code> on Linux and macOS, or an Administrator PowerShell on Windows; ESP32 uses its setup page). In the prototype, use the review-only “Simulate” control. A collector that is not reporting queues the test (see <a href="#c-gate">correction 2</a>). <code>&amp;source=&lt;id&gt;</code> resumes a draft.</p>
        <ol class="rv-steps">
          <li><span><strong>Success</strong> · Remote NUT server <code>192.168.1.20</code>, UPS name <code>rackups</code> (identity <code>broadwater-lan|nut|192.168.1.20|3493|rackups</code>)</span><span class="rv-why">Also SNMP v3 <code>192.168.1.25</code> (APC SRT 2200). <code>deskups</code> on the same server is a second UPS (<a href="#c-id">correction 1</a>).</span></li>
          <li><span><strong>Unreachable endpoint</strong> · NUT <code>192.168.1.99</code></span></li>
          <li><span><strong>Rejected credentials</strong> · NUT <code>192.168.1.22</code>, UPS name <code>office</code> (or choose “Entered a wrong secret” in the simulator)</span></li>
          <li><span><strong>Unsupported capability (partial)</strong> · SNMP v2c <code>192.168.1.26</code></span></li>
          <li><span><strong>Already monitored</strong> · SNMP <code>10.0.20.15</code> (same identity <code>broadwater-lan|snmp|10.0.20.15|161|</code>) → blocked before any draft: “Already monitored by bw-pi-01 as BW Office UPS”. No second asset is created. The same address in another namespace is a different identity.</span></li>
          <li><span><strong>Possibly the same UPS</strong> · SNMP <code>10.0.20.16</code> → after Identify: “Possibly the same UPS as BW Rack UPS (serial match) — confirm”. Continue or cancel; never a block.</span></li>
        </ol>
        <ul class="rv-links">
          <li><a href="app/sources.html?collector=bw-pi-01">Sources · “Add network UPS”</a></li>
          <li><a href="app/network-source.html?collector=home-esp32-garage">ESP32 collector (not applicable)</a></li>
          <li><a href="app/network-source.html?collector=bw-pi-01&amp;scenario=revoked">Revoked collector</a></li>
          <li><a href="cli/index.html#g-network">CLI · network UPS</a><span class="rv-why"><a href="cli/examples/sources-add-nut.80.txt">add</a> · <a href="cli/examples/sources-local-access-prompt.80.txt">local access prompt</a> · <a href="cli/examples/sources-test-ok.80.txt">test ok</a> · <a href="cli/examples/sources-add-unreachable.80.txt">unreachable (exit 6)</a> · <a href="cli/examples/sources-access-rejected.80.txt">rejected (exit 6)</a> · <a href="cli/examples/sources-test-partial.80.txt">partial (exit 10)</a> · <a href="cli/examples/sources-add-duplicate.80.txt">duplicate (exit 1)</a> · <a href="cli/examples/sources-test-serial-match.80.txt">serial match (exit 10)</a> · <a href="cli/examples/sources-use.80.txt">use</a> · <a href="cli/examples/sources-use-waiting.80.txt">use, waiting for UPS role (exit 0)</a></span></li>
        </ul>
        <p class="rv-note">Onboarding branch: journey 1, step 7.</p>
      </article>

      <article class="rv-card" aria-labelledby="j7">
        <h3 id="j7">7 · Witness down vs site contact lost vs locally observed connectivity failure</h3>
        <ol class="rv-steps">
          <li><a href="app/index.html?scenario=witness-down">Fleet · witness down</a><span class="rv-why">Home: “Site still reachable: home-esp32-garage reporting”</span></li>
          <li><a href="app/site.html?id=home&amp;scenario=witness-down">Home site · witness down</a></li>
          <li><a href="app/incident.html?id=INC-0415&amp;scenario=witness-down">INC-0415 · Collector not reporting — home-studio</a></li>
          <li><a href="app/collector.html?id=home-studio&amp;scenario=witness-down">home-studio · last contact 15:04:12</a></li>
          <li><a href="app/collectors.html?scenario=witness-down">Collectors · witness down</a></li>
          <li><a href="app/relationships.html?site=home&amp;focus=col:home-studio&amp;scenario=witness-down">Relationships · Home, home-studio focused</a></li>
          <li><a href="app/site.html?id=shop">Shop · site contact lost (all witnesses silent, cause unknown)</a><span class="rv-why"><a href="app/incident.html?id=INC-0413">INC-0413</a> · <a href="app/relationships.html?focus=site:shop">relationships focus</a></span></li>
          <li><a href="app/site.html?id=duplex">Duplex · locally observed connectivity failure (probe evidence 13:41)</a><span class="rv-why"><a href="app/incident.html?id=INC-0411">INC-0411</a></span></li>
        </ol>
        <p class="rv-note">Compare the two silence panels: <a href="app/incident.html?id=INC-0413">INC-0413</a> and <a href="app/incident.html?id=INC-0411">INC-0411</a>.</p>
      </article>

      <article class="rv-card" aria-labelledby="j8">
        <h3 id="j8">8 · Workstation sleep</h3>
        <ol class="rv-steps">
          <li><a href="app/index.html">Fleet counts</a><span class="rv-why">“4 of 6 expected collectors reporting · 1 workstation asleep”</span></li>
          <li><a href="app/collector.html?id=home-laptop">home-laptop · Asleep (ordinary workstation) — no incident</a><span class="rv-why">Asleep since 13:05; last contact 13:05:40</span></li>
          <li><a href="app/collector-config.html?id=home-laptop">Configuration · change pending until next check-in</a><span class="rv-why">Review controls: Simulate collector reconnect / check-in / disconnect. Full gating journey: <a href="#c-gate">correction 2</a>.</span></li>
          <li><a href="app/collectors.html">Collectors list</a></li>
          <li><a href="app/relationships.html?focus=eq:bw-desk-ws">bw-desk-ws · relationship only (no agent, normal)</a></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="j9">
        <h3 id="j9">9 · Equipment-filtered history, including 7-day aggregation</h3>
        <ol class="rv-steps">
          <li><a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">Broadwater · BW Office UPS · 24 h</a><span class="rv-why">BW Rack’s battery episode appears only under “Site context”</span></li>
          <li><a href="app/ups.html?id=bw-office-ups">BW Office UPS detail · site context separate</a></li>
          <li><a href="app/history.html?site=shop&amp;range=7d">Shop · 7 d hourly aggregates</a><span class="rv-why">“No records” 26 Sep 02:00–06:00</span></li>
          <li><a href="app/history.html?site=shop&amp;ups=shop-bench-ups&amp;range=7d">Shop Bench UPS · 7 d</a></li>
          <li><a href="app/ups.html?id=shop-bench-ups&amp;range=7d">UPS detail · 7 d gap</a></li>
          <li><a href="app/ups.html?id=bw-rack-ups&amp;range=7d">BW Rack UPS · 7 d</a></li>
          <li><a href="app/equipment.html?cmp=1&amp;pick=bw-rack-ups,shop-bench-ups,garage-ups">Equipment compare (3 UPSs)</a></li>
        </ol>
      </article>

      <article class="rv-card" aria-labelledby="j10">
        <h3 id="j10">10 · Mobile incident detail · acknowledge</h3>
        <ol class="rv-steps">
          <li><a href="app/incident.html?id=INC-0413">INC-0413 on a phone (≤ 767 px)</a><span class="rv-why">Fixed action bar: check → evidence → acknowledge (bottom sheet)</span></li>
          <li><span>Acknowledge with a note (changes state). Then open <a href="app/incidents.html">incidents</a> and the <a href="app/index.html">fleet</a>: they show the same record.</span></li>
          <li><a href="app/incident.html?id=INC-0412">INC-0412 on a phone</a></li>
        </ol>
        <p class="rv-note">The action bar is in source only. It has not been checked at 375 px or on a device.</p>
      </article>
    </div>
  </section>

  <!-- c. Configuration ownership -->
  <section class="rv-sec" id="ownership" aria-labelledby="h-own">
    <p class="mk-eyebrow">c · Configuration ownership</p>
    <h2 id="h-own">Configuration ownership (one table for web and CLI)</h2>
    <p class="rv-note">The table below comes from <code>MK.config.contract</code> when the shared scripts load; otherwise it shows a summary. The same rows are used by <a href="app/collector-config.html?id=bw-pi-01">collector-config.html?id=bw-pi-01</a> and the <a href="cli/index.html#ownership">CLI configuration-ownership section</a>. Override example on bw-pi-01: “Effective: 15 s (local override) · Desired in dashboard: 30 s”. Rows are platform-neutral here; each collector’s configuration page resolves paths, elevation and secret store for its own platform (<code>MK.config.contractFor</code>). Other states: <a href="app/collector-config.html?id=shop-esp32-net">failed (shop-esp32-net, max 3 targets)</a> · <a href="app/collector-config.html?id=home-laptop">pending (home-laptop asleep)</a>.</p>
    <div class="mk-table-wrap" style="--mk-table-max-h:none" tabindex="0" role="region" aria-label="Configuration ownership table">
      <table class="mk-table rv-table" style="--mk-table-min:860px" id="rv-own">
        <caption class="mk-visually-hidden">Configuration ownership: setting, owner, edit location, effective value source, application behaviour</caption>
        <thead><tr><th scope="col">Setting</th><th scope="col">Owner</th><th scope="col">Edit location</th><th scope="col">Effective value source</th><th scope="col">Application behaviour</th></tr></thead>
        <tbody id="rv-own-body">
          <tr><td>Site, display name, roles, expected availability, heartbeat interval, reporting interval, connectivity targets</td><td>Dashboard</td><td>Dashboard › Collectors › collector › Configuration</td><td>Dashboard (heartbeat: unless a local override is set)</td><td>Desired → pending → applied on next check-in, or failed with a reason</td></tr>
          <tr><td>Alert rules &amp; notification thresholds</td><td>Dashboard (server)</td><td>Dashboard › Settings › Alert rules</td><td>Server</td><td>Server-side, immediate</td></tr>
          <tr><td>NUT credentials, SNMP v3 secrets, SNMP community</td><td>Local secret</td><td>On the collector: <code>mainskeeper sources auth &lt;source&gt;</code>, elevated per platform (ESP32: its local setup page)</td><td>The platform’s secret store (macOS Keychain · /etc/mainskeeper/secrets.d · Windows DPAPI · on the device)</td><td>Dashboard shows only Configured / Missing / Rejected + last test time</td></tr>
          <tr><td>Storage path, disk cap, retention target, log level, log path</td><td>Local</td><td>The platform’s <code>collector.toml</code> (macOS /Library/Application Support/MainsKeeper · Linux /etc/mainskeeper · Windows C:\ProgramData\MainsKeeper) or an elevated <code>mainskeeper config set</code></td><td>Collector</td><td>Read-only in the dashboard, “Set locally”</td></tr>
          <tr><td>USB host, flash ring, UPS classes, max targets</td><td>Capability</td><td>Not editable</td><td>Hardware</td><td>Shown as capabilities; ESP32 values illustrative</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- d. Product map -->
  <section class="rv-sec" id="map" aria-labelledby="h-map">
    <p class="mk-eyebrow">d · Product map</p>
    <h2 id="h-map">Product map</h2>
    <p class="rv-note">Every page, what it is for, and direct links to its states. Links without <code>state=</code> or <code>scenario=</code> open the active dataset (the demo incident fixture unless you switched to your account). Add <code>&amp;theme=light</code> to any link for the light theme.</p>
    <div class="rv-grid">

      <article class="rv-card" aria-labelledby="pm-fleet">
        <h3 id="pm-fleet">Fleet &amp; sites</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/index.html">index.html — Fleet overview</a></span><span class="rv-page__purpose">Landing page. Sites with reachability, open incidents, recoveries, history sync; counts derived from the merged data.</span>
          <span class="rv-states"><a href="app/index.html?state=incident">Incident</a><a href="app/index.html?state=calm">Calm</a><a href="app/index.html?loading=1">Loading</a><a href="app/index.html?scenario=empty">Empty account</a><a href="app/index.html?scenario=ups-comms-lost">UPS comms lost</a><a href="app/index.html?scenario=revoked">Collector revoked</a><a href="app/index.html?scenario=witness-down">Witness down</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/site.html?id=broadwater">site.html — Site detail</a></span><span class="rv-page__purpose">UPS cards by circuit, collectors with contact state, reachability, connectivity, history, availability with coverage.</span>
          <span class="rv-states"><a href="app/site.html?id=broadwater">Broadwater (on battery)</a><a href="app/site.html?id=home">Home</a><a href="app/site.html?id=home&amp;scenario=witness-down">Home · witness down</a><a href="app/site.html?id=shop">Shop (site contact lost)</a><a href="app/site.html?id=duplex">Duplex (local probe)</a><a href="app/site.html?id=nope">Not found</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/site-setup.html?id=home">site-setup.html — Site settings</a></span><span class="rv-page__purpose">Name and time zone edits; witness changes go through the desired → pending lifecycle; some expectations read-only.</span>
          <span class="rv-states"><a href="app/site-setup.html?id=home">Edit Home</a><a href="app/site-setup.html?mode=new">New site</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-eq">
        <h3 id="pm-eq">Equipment &amp; UPS</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/equipment.html">equipment.html — Equipment workspace</a></span><span class="rv-page__purpose">Filter, sort and compare UPSs; freshness column; state in the URL.</span>
          <span class="rv-states"><a href="app/equipment.html">All UPSs</a><a href="app/equipment.html?cond=on-battery">On battery</a><a href="app/equipment.html?cond=stale">Stale</a><a href="app/equipment.html?cond=overdue">Overdue</a><a href="app/equipment.html?cond=waiting">Waiting for first reading</a><a href="app/equipment.html?cmp=1&amp;pick=bw-rack-ups,shop-bench-ups,garage-ups">Compare 3</a><a href="app/equipment.html?cond=unsupported">Unsupported readings</a><a href="app/equipment.html?scenario=empty">No UPSs</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/ups.html?id=bw-rack-ups">ups.html — UPS detail</a></span><span class="rv-page__purpose">Readings with freshness and staleness legend, UPS estimate vs observed duration, charts (24 h minute data, 7 d hourly aggregates), protected equipment.</span>
          <span class="rv-states"><a href="app/ups.html?id=bw-rack-ups">BW Rack (on battery)</a><a href="app/ups.html?id=bw-rack-ups&amp;range=7d">BW Rack · 7 d</a><a href="app/ups.html?id=bw-office-ups">BW Office (600 s cadence · On schedule · site context separate)</a><a href="app/ups.html?id=office-desk-ups&amp;scenario=witness-down">Office Desk · witness down (unknown after 15:04:12)</a><a href="app/ups.html?id=office-desk-ups">Office Desk (38 s, event only)</a><a href="app/ups.html?id=shop-bench-ups">Shop Bench (stale · last known)</a><a href="app/ups.html?id=shop-bench-ups&amp;range=7d">Shop Bench · 7 d gap</a><a href="app/ups.html?id=garage-ups&amp;scenario=ups-comms-lost">Garage (comms lost)</a><a href="app/ups.html?id=garage">Not found</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-inc">
        <h3 id="pm-inc">Incidents &amp; notifications</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/incidents.html">incidents.html — Incidents</a></span><span class="rv-page__purpose">Open, recovered and resolved incidents; acknowledgments from the shared store.</span>
          <span class="rv-states"><a href="app/incidents.html">Open</a><a href="app/incidents.html?tab=recovered">Recovered</a><a href="app/incidents.html?tab=resolved">Resolved</a><a href="app/incidents.html?nfail=1">Notification failures</a><a href="app/incidents.html?state=calm">Calm</a><a href="app/incidents.html?sev=info&amp;site=home">Empty filter result</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/incident.html?id=INC-0412">incident.html — Incident detail</a></span><span class="rv-page__purpose">Six-label timestamps row, evidence timeline, notifications, acknowledgment with note, silence panels.</span>
          <span class="rv-states"><a href="app/incident.html?id=INC-0412">INC-0412 on battery</a><a href="app/incident.html?id=INC-0413">INC-0413 contact lost</a><a href="app/incident.html?id=INC-0411">INC-0411 recovered</a><a href="app/incident.html?id=INC-0409">INC-0409 resolved</a><a href="app/incident.html?id=INC-0414&amp;scenario=ups-comms-lost">INC-0414 UPS comms lost</a><a href="app/incident.html?id=INC-0415&amp;scenario=witness-down">INC-0415 witness down</a><a href="app/incident.html?id=NOPE">Not found</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/notifications.html">notifications.html — Notifications</a></span><span class="rv-page__purpose">Channels and the delivery log.</span>
          <span class="rv-states"><a href="app/notifications.html">All</a><a href="app/notifications.html?channel=ch-push">Push channel (failing)</a><a href="app/notifications.html?status=failed">Failed</a><a href="app/notifications.html?status=retrying">Retrying</a><a href="app/notifications.html?status=suppressed">Suppressed</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/maintenance.html">maintenance.html — Maintenance windows</a></span><span class="rv-page__purpose">Windows stored in the shared store.</span>
          <span class="rv-states"><a href="app/maintenance.html">List</a><a href="app/maintenance.html?new=1&amp;site=home">New · Home</a><a href="app/maintenance.html?edit=mw-shop-network">Edit Shop window</a><a href="app/maintenance.html?new=1&amp;site=shop&amp;date=2026-10-03&amp;start=09:00&amp;end=11:00">New · overlap warning</a><a href="app/maintenance.html?new=1&amp;date=2026-10-02&amp;start=10:00&amp;end=09:00">New · invalid range</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/alert-rules.html">alert-rules.html — Alert rules (Settings)</a></span><span class="rv-page__purpose">Delays and thresholds configure notifications only; server-side, immediate.</span>
          <span class="rv-states"><a href="app/alert-rules.html">List</a><a href="app/alert-rules.html?rule=rule-on-battery">On battery · after 60 s · Home 38 s example</a><a href="app/alert-rules.html?rule=rule-low-runtime">Low runtime</a><a href="app/alert-rules.html?rule=rule-low-runtime&amp;threshold=0">Validation error</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-hist">
        <h3 id="pm-hist">History</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/history.html">history.html — History and investigation</a></span><span class="rv-page__purpose">Site, UPS and range filter charts, episodes and events alike; “Site context” section; 7 d hourly aggregates with coverage.</span>
          <span class="rv-states"><a href="app/history.html?site=duplex&amp;from=13:30&amp;to=15:30">Duplex 13:30–15:30 (replay)</a><a href="app/history.html?site=duplex&amp;ep=ep-duplex-today">Duplex episode</a><a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">BW Office · site context</a><a href="app/history.html?site=shop&amp;range=7d">Shop · 7 d</a><a href="app/history.html?site=shop&amp;ups=shop-bench-ups&amp;range=7d">Shop Bench · 7 d</a><a href="app/history.html?site=duplex&amp;from=15:00&amp;to=15:22">Duplex 15:00–15:22</a><a href="app/history.html?from=15:00&amp;to=15:22">All sites 15:00–15:22</a><a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22">Office Desk · witness down</a><a href="app/history.html?range=1h">1 h</a><a href="app/history.html?range=24h">24 h</a><a href="app/history.html?range=7d">7 d (all sites)</a><a href="app/history.html?site=home&amp;from=10:00&amp;to=11:00">Empty range</a><a href="app/history.html?from=16:00&amp;to=17:00">Invalid range</a><a href="app/history.html?demo=loading">Loading</a><a href="app/history.html?site=nope">Not found</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-col">
        <h3 id="pm-col">Collectors, coverage &amp; sources</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/collectors.html">collectors.html — Collectors</a></span><span class="rv-page__purpose">Contact state, roles, site witnesses vs workstations, buffers, secrets status.</span>
          <span class="rv-states"><a href="app/collectors.html">All</a><a href="app/collectors.html?scenario=witness-down">Witness down</a><a href="app/collectors.html?role=ups&amp;status=contact-lost">UPS role · contact lost</a><a href="app/collectors.html?site=shop">Shop</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/collector.html?id=bw-pi-01">collector.html — Collector detail</a></span><span class="rv-page__purpose">Contact vs reading age, staleness legend, roles, sources, targets, buffer, capabilities.</span>
          <span class="rv-states"><a href="app/collector.html?id=bw-pi-01">bw-pi-01</a><a href="app/collector.html?id=home-laptop">home-laptop (asleep)</a><a href="app/collector.html?id=home-studio&amp;scenario=witness-down">home-studio (witness down)</a><a href="app/collector.html?id=shop-esp32-net">shop-esp32-net (illustrative ESP32)</a><a href="app/collector.html?id=duplex-pi-probe">duplex-pi-probe (draining)</a><a href="app/collector.html?id=bw-pi-01&amp;scenario=revoked">bw-pi-01 revoked</a><a href="app/collector.html?id=nope">Not found</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/relationships.html">relationships.html — Relationships and coverage</a></span><span class="rv-page__purpose">Reads / powers / checks map; <code>?site=</code> filters clusters, <code>?focus=kind:id</code> preselects.</span>
          <span class="rv-states"><a href="app/relationships.html">Map</a><a href="app/relationships.html?focus=col:bw-pi-01">Focus bw-pi-01</a><a href="app/relationships.html?focus=site:shop">Focus Shop</a><a href="app/relationships.html?site=home&amp;focus=col:home-studio&amp;scenario=witness-down">Home · witness down</a><a href="app/relationships.html?focus=eq:bw-desk-ws">Sleeping workstation</a><a href="app/relationships.html?cov=missing">Coverage: missing</a><a href="app/relationships.html?focus=ups:nope">Unknown focus</a><a href="app/relationships.html?site=nope">Unknown site</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/sources.html">sources.html — UPS sources</a></span><span class="rv-page__purpose">Overview without a collector; per-collector discovery, choice, secrets status and diagnostics; “Add network UPS”.</span>
          <span class="rv-states"><a href="app/sources.html">Overview</a><a href="app/sources.html?collector=bw-pi-01">bw-pi-01 · live</a><a href="app/sources.html?collector=bw-pi-01&amp;case=credentials-rejected">Credentials rejected</a><a href="app/sources.html?collector=bw-pi-01&amp;case=unreachable">Unreachable</a><a href="app/sources.html?case=none-found">None found</a><a href="app/sources.html?case=multiple-found">Multiple found</a><a href="app/sources.html?case=existing-service">Existing NUT service</a><a href="app/sources.html?case=owned-by-other-driver">Device busy</a><a href="app/sources.html?case=partial-capability">Partial capability</a><a href="app/sources.html?case=comms-failing">Comms failing</a><a href="app/sources.html?collector=nope">Not found</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/network-source.html?collector=bw-pi-01">network-source.html — Add network UPS (NUT / SNMP)</a></span><span class="rv-page__purpose">Protocol → endpoint (identity and hard-duplicate check) → local credentials (status only) → test (queued while the collector is not reporting) → identify → serial-match confirm → use (monitoring, or “Configured — waiting …”).</span>
          <span class="rv-states"><a href="app/network-source.html?collector=bw-pi-01">bw-pi-01</a><a href="app/network-source.html?collector=home-studio&amp;state=calm">home-studio (home-lan)</a><a href="app/network-source.html?collector=home-laptop">home-laptop (asleep · queued test)</a><a href="app/network-source.html?collector=home-esp32-garage">ESP32 (not applicable)</a><a href="app/network-source.html?collector=bw-pi-01&amp;scenario=revoked">Revoked</a><a href="app/network-source.html?collector=nope">Not found</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/collector-config.html?id=bw-pi-01">collector-config.html — Collector configuration</a></span><span class="rv-page__purpose">Ownership table, desired/pending/applied/failed, override, secrets status, local read-only, capabilities.</span>
          <span class="rv-states"><a href="app/collector-config.html?id=bw-pi-01">bw-pi-01 (override)</a><a href="app/collector-config.html?id=shop-esp32-net">shop-esp32-net (failed)</a><a href="app/collector-config.html?id=home-laptop">home-laptop (pending)</a><a href="app/collector-config.html?id=home-esp32-garage">home-esp32-garage (ESP32)</a><a href="app/collector-config.html?id=bw-pi-01&amp;scenario=revoked">bw-pi-01 revoked</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-sync">
        <h3 id="pm-sync">Sync</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/sync.html">sync.html — History sync</a></span><span class="rv-page__purpose">Per-collector buffer (<code>?collector=</code>); illustrative cases; sync never holds an incident open.</span>
          <span class="rv-states"><a href="app/sync.html?collector=duplex-pi-probe">duplex-pi-probe</a><a href="app/sync.html?case=draining">Draining</a><a href="app/sync.html?case=healthy">Healthy</a><a href="app/sync.html?case=offline">Offline · recording</a><a href="app/sync.html?case=replay-live">Replay + live</a><a href="app/sync.html?case=stalled">Stalled</a><a href="app/sync.html?case=pressure">Storage pressure</a><a href="app/sync.html?case=lost">Records lost</a><a href="app/sync.html?collector=nope">Not found</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-ob">
        <h3 id="pm-ob">Onboarding &amp; enrollment</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/sign-in.html?auth=signed-out">sign-in.html — Sign-in</a></span><span class="rv-page__purpose">Cloudflare Access with GitHub; <code>?auth=</code> states (never <code>?state=</code>).</span>
          <span class="rv-states"><a href="app/sign-in.html?auth=signed-out">Signed out</a><a href="app/sign-in.html?auth=redirecting">Redirecting</a><a href="app/sign-in.html?auth=callback">Callback</a><a href="app/sign-in.html?auth=access-denied">Access denied</a><a href="app/sign-in.html?auth=expired-session&amp;return=incident.html%3Fid%3DINC-0412">Session expired</a><a href="app/sign-in.html?auth=error">Error</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/welcome.html?scenario=empty">welcome.html — First run (8 steps)</a></span><span class="rv-page__purpose">Empty account to first readings, in the account dataset. See journey 1.</span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/install.html?platform=pi">install.html — Install a collector</a></span><span class="rv-page__purpose">Per-platform install with elevation. Installers are design targets.</span>
          <span class="rv-states"><a href="app/install.html?platform=macos">macOS</a><a href="app/install.html?platform=windows">Windows</a><a href="app/install.html?platform=fedora">Fedora</a><a href="app/install.html?platform=pi">Raspberry Pi</a><a href="app/install.html?platform=esp32">ESP32 probe</a><a href="app/install.html?platform=bogus">Unknown platform</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/enroll.html?enroll=waiting">enroll.html — Enrollment</a></span><span class="rv-page__purpose">Short user code, browser approval. Expired, denied and offline are display-only.</span>
          <span class="rv-states"><a href="app/enroll.html?enroll=waiting">Waiting</a><a href="app/enroll.html?enroll=confirm&amp;code=BKTN-XRWP&amp;scenario=empty">Confirm</a><a href="app/enroll.html?enroll=expired">Expired</a><a href="app/enroll.html?enroll=denied">Denied</a><a href="app/enroll.html?enroll=offline">Offline</a><a href="app/enroll.html?enroll=revoked&amp;scenario=revoked">Revoked</a><a href="app/enroll.html?enroll=reenroll&amp;code=KQPT-RNXV&amp;scenario=revoked">Re-enroll</a><a href="app/enroll.html?enroll=confirm&amp;code=ZZZZ-ZZZZ">Unknown code</a></span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-cli">
        <h3 id="pm-cli">CLI</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="cli/index.html">cli/index.html — Proposed CLI contract</a></span><span class="rv-page__purpose">One grammar with elevation, configuration ownership, network UPS, exit codes, JSON schema and terminal mockups. Monitoring only.</span>
          <span class="rv-states"><a href="cli/index.html#commands">Commands</a><a href="cli/index.html#elevation">Elevation</a><a href="cli/index.html#ownership">Configuration ownership</a><a href="cli/index.html#g-network">Network UPS</a><a href="cli/index.html#g-gating">Gating</a><a href="cli/index.html#platforms">Platforms</a><a href="cli/index.html#exit">Exit codes</a><a href="cli/index.html#json-schema">JSON schema</a><a href="cli/index.html#mockups">Mockups</a><a href="cli/index.html?w=80">80 col</a><a href="cli/index.html?w=120">120 col</a><a href="cli/index.html?term=light">Light</a><a href="cli/index.html?term=nocolor">NO_COLOR</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="#cli-examples">Examples (151 files, all in the checkpoint)</a></span><span class="rv-page__purpose">Plain text at 80/120 columns and JSON, by category.</span></div>
      </article>

      <article class="rv-card" aria-labelledby="pm-ds">
        <h3 id="pm-ds">Design system</h3>
        <div class="rv-page"><span class="rv-page__name"><a href="app/components.html">components.html — Component gallery</a></span><span class="rv-page__purpose">Every shared component in its important states, both themes.</span>
          <span class="rv-states"><a href="app/components.html">Both</a><a href="app/components.html?view=dark">Dark</a><a href="app/components.html?view=light">Light</a></span></div>
        <div class="rv-page"><span class="rv-page__name"><a href="app/assets/tokens.css">tokens.css</a> · <a href="app/assets/CONTRACT.md">CONTRACT.md</a> · <a href="brand/README.md">brand README</a></span><span class="rv-page__purpose">Colour roles, type, spacing, motion, contrast tables, store and policy contracts.</span></div>
      </article>
    </div>
  </section>

  <!-- e. State coverage -->
  <section class="rv-sec" id="coverage" aria-labelledby="h-cov">
    <p class="mk-eyebrow">e · State coverage</p>
    <h2 id="h-cov">State-coverage checklist</h2>
    <p class="rv-note"><span class="rv-status rv-status--designed">Designed</span> = built in prototype source and linked. <span class="rv-status rv-status--partial">Partial</span> = built with a stated gap. <span class="rv-status rv-status--not">Not designed</span> = absent. No row has been visually verified. <span id="rv-cov-count">Totals: 51 Designed · 8 Partial · 1 Not designed.</span></p>
    <div class="mk-table-wrap" style="--mk-table-max-h:none" tabindex="0" role="region" aria-label="State coverage table">
      <table class="mk-table rv-table" style="--mk-table-min:860px" id="rv-cov">
        <caption class="mk-visually-hidden">State coverage: state, where to see it, status, notes</caption>
        <thead><tr><th scope="col">State</th><th scope="col">Where</th><th scope="col">Status</th><th scope="col">Notes</th></tr></thead>
        <tbody>
          <tr><td>First use · no devices</td><td class="rv-where"><a href="app/index.html?scenario=empty">Fleet empty</a><a href="app/welcome.html?scenario=empty">Welcome</a><a href="app/equipment.html?scenario=empty">Equipment empty</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Account dataset, empty until you create a site.</td></tr>
          <tr><td>First run to your own fleet (same collector and UPS)</td><td class="rv-where"><a href="app/welcome.html?scenario=empty">Welcome</a><a href="app/collector.html?id=garage-pi-02">garage-pi-02</a><a href="app/ups.html?id=cyberpower-cp1500pfclcd">CyberPower UPS</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Created entities live in the store. These ids exist only after you walk journey 1. Before that, they show “not found”.</td></tr>
          <tr><td>New collector after approval (sources, configuration)</td><td class="rv-where"><a href="app/sources.html?collector=garage-pi-02">Sources</a><a href="app/collector-config.html?id=garage-pi-02">Configuration</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>No silent fallback to bw-pi-01.</td></tr>
          <tr><td>Dataset switch · Reset demo</td><td class="rv-where"><a href="app/index.html">Review bar on any page</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Reset asks for confirmation and removes entity params that no longer exist.</td></tr>
          <tr><td>Not-found states</td><td class="rv-where"><a href="app/site.html?id=nope">Site</a><a href="app/ups.html?id=garage">UPS</a><a href="app/incident.html?id=NOPE">Incident</a><a href="app/sources.html?collector=nope">Sources</a><a href="app/sync.html?collector=nope">Sync</a><a href="app/relationships.html?site=nope">Relationships</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Never substitutes another entity.</td></tr>
          <tr><td>Parameter contract</td><td class="rv-where"><a href="app/sign-in.html?auth=callback">sign-in ?auth=</a><a href="app/relationships.html?focus=col:bw-pi-01">?focus=</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Carried: state, scenario, theme, motion. Not carried: auth, enroll, id, site, collector, ups, focus.</td></tr>
          <tr><td>Initial loading</td><td class="rv-where"><a href="app/index.html?loading=1">Fleet</a><a href="app/history.html?demo=loading">History</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Skeletons only on fleet and history.</td></tr>
          <tr><td>Healthy</td><td class="rv-where"><a href="app/index.html?state=calm">Fleet calm</a><a href="app/site.html?id=home">Home</a><a href="app/sync.html?case=healthy">Sync healthy</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Partial capabilities (unsupported readings)</td><td class="rv-where"><a href="app/ups.html?id=bw-office-ups">BW Office UPS</a><a href="app/sources.html?case=partial-capability">Source</a><a href="app/equipment.html?cond=unsupported">Equipment</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>“Unsupported” / “Not reported”, never 0.</td></tr>
          <tr><td>On battery (notify after 60 s)</td><td class="rv-where"><a href="app/ups.html?id=bw-rack-ups">BW Rack UPS</a><a href="app/incident.html?id=INC-0412">INC-0412</a><a href="app/alert-rules.html?rule=rule-on-battery">Rule</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Home 38 s episode: “Below 60 s delay — event only”.</td></tr>
          <tr><td>Low runtime</td><td class="rv-where"><a href="app/alert-rules.html?rule=rule-low-runtime">Rule</a><a href="app/ups.html?id=bw-rack-ups">BW Rack UPS</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>The rule is designed. No fixture UPS falls below 10 min, and the round-4 page-local demo was removed.</td></tr>
          <tr><td>Utility restoration</td><td class="rv-where"><a href="app/incident.html?id=INC-0412&amp;step=restored">INC-0412 restored</a><a href="app/incident.html?id=INC-0412&amp;step=resolved">Resolved</a><a href="app/history.html?site=duplex&amp;ep=ep-duplex-today">Unknown restore time</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Store-driven (clock 15:32 / 15:37); every page agrees.</td></tr>
          <tr><td>Acknowledged</td><td class="rv-where"><a href="app/incident.html?id=INC-0412&amp;step=ack">INC-0412 ack</a><a href="app/incidents.html">Incidents</a><a href="app/incident.html?id=INC-0411">INC-0411</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>One record per incident in the shared store.</td></tr>
          <tr><td>Incident timestamps (six labels)</td><td class="rv-where"><a href="app/incident.html?id=INC-0413">INC-0413</a><a href="app/incident.html?id=INC-0411">INC-0411</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Resolved = recovered + 5 min stable (decision to confirm).</td></tr>
          <tr><td>Witness down (collector failure, site still reachable)</td><td class="rv-where"><a href="app/site.html?id=home&amp;scenario=witness-down">Home</a><a href="app/incident.html?id=INC-0415&amp;scenario=witness-down">INC-0415</a><a href="app/collector.html?id=home-studio&amp;scenario=witness-down">home-studio</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>“Site still reachable: home-esp32-garage reporting”.</td></tr>
          <tr><td>Site contact lost vs locally observed connectivity failure</td><td class="rv-where"><a href="app/site.html?id=shop">Shop · cause unknown</a><a href="app/site.html?id=duplex">Duplex · local probe</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>UPS comms lost while the collector reports</td><td class="rv-where"><a href="app/ups.html?id=garage-ups&amp;scenario=ups-comms-lost">Garage UPS</a><a href="app/incident.html?id=INC-0414&amp;scenario=ups-comms-lost">INC-0414</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Workstation asleep (normal)</td><td class="rv-where"><a href="app/collector.html?id=home-laptop">home-laptop</a><a href="app/relationships.html?focus=eq:bw-desk-ws">bw-desk-ws</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Excluded from expected collector counts.</td></tr>
          <tr><td>Freshness: On schedule · Overdue · Stale (per cadence)</td><td class="rv-where"><a href="app/ups.html?id=shop-bench-ups">Shop Bench UPS</a><a href="app/equipment.html?cond=stale">Equipment stale</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Relative to the reporting interval (30 s: ≤ 60 s · ≤ 90 s · &gt; 90 s; 600 s: ≤ 15 · ≤ 25 · &gt; 25 min), with absolute age and a legend. No fixture UPS is currently Overdue.</td></tr>
          <tr><td>Recovery with uploads draining</td><td class="rv-where"><a href="app/incident.html?id=INC-0411">INC-0411</a><a href="app/sync.html?collector=duplex-pi-probe">Sync</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Historical backfill and replay</td><td class="rv-where"><a href="app/history.html?site=duplex&amp;from=13:30&amp;to=15:30">Duplex history</a><a href="app/sync.html?case=replay-live">Replay + live</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Exact policy text; replay = &gt; 2 min late while reconnecting.</td></tr>
          <tr><td>Storage limits</td><td class="rv-where"><a href="app/sync.html?case=pressure">Pressure</a><a href="app/sync.html?case=lost">Records lost</a><a href="cli/examples/queue-pressure.80.txt">CLI</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>ESP32 values illustrative.</td></tr>
          <tr><td>History gaps · uncertain timestamps</td><td class="rv-where"><a href="app/history.html?site=duplex&amp;from=13:30&amp;to=15:30">Duplex 14:30–14:52</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Hatched “No records”; ±90 s until clock sync.</td></tr>
          <tr><td>History filtered by UPS (site context separate)</td><td class="rv-where"><a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">BW Office UPS</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Charts: 7-day hourly aggregates with coverage</td><td class="rv-where"><a href="app/history.html?site=shop&amp;range=7d">Shop 7 d</a><a href="app/ups.html?id=shop-bench-ups&amp;range=7d">UPS 7 d</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Custom ranges are limited to the last 7 days. The all-sites 7 d SVG is heavy.</td></tr>
          <tr><td>Chart range selection on touch</td><td class="rv-where"><a href="app/history.html">History</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Drag selection off on touch; use the range form.</td></tr>
          <tr><td>Network UPS setup (outcomes + identity)</td><td class="rv-where"><a href="app/network-source.html?collector=bw-pi-01">network-source.html</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Success, unreachable, rejected, unsupported, already monitored (same identity), serial-match confirm.</td></tr>
          <tr><td>Network identity: same host, different UPS / namespace</td><td class="rv-where"><a href="app/network-source.html?collector=bw-pi-01">bw-pi-01 rackups + deskups</a><a href="app/network-source.html?collector=home-studio&amp;state=calm">home-studio home-lan</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Correction 1 (a)/(b). Untrustworthy serial 000000000000 decides nothing.</td></tr>
          <tr><td>Test queued (collector not reporting)</td><td class="rv-where"><a href="app/network-source.html?collector=home-laptop">home-laptop</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>“Test queued — collector not reporting”; runs at the next check-in. No CLI screen (CLI runs on the collector).</td></tr>
          <tr><td>Configured — waiting for UPS role / for collector</td><td class="rv-where"><a href="app/collector-config.html?id=home-laptop">home-laptop configuration</a><a href="app/collector.html?id=home-laptop">home-laptop</a><a href="cli/examples/sources-use-waiting.80.txt">CLI</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Store-enforced; readings only after the role applies and the collector reports.</td></tr>
          <tr><td>Unknown evidence after a witness goes silent</td><td class="rv-where"><a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22">History</a><a href="app/ups.html?id=office-desk-ups&amp;scenario=witness-down">UPS</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Nothing after 15:04:12; unknown in lanes, charts, aggregates, coverage and availability.</td></tr>
          <tr><td>Awaiting next report (600 s cadence)</td><td class="rv-where"><a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">BW Office UPS</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>“Not reported yet — next report due by 15:25:00 (every 600 s)”.</td></tr>
          <tr><td>Platform instructions (Windows, ESP32, macOS, Pi/Fedora)</td><td class="rv-where"><a href="app/install.html?platform=windows">Windows</a><a href="app/install.html?platform=esp32">ESP32</a><a href="app/install.html?platform=macos">macOS</a><a href="cli/index.html#platforms">CLI</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>From MK.cli.platform; OS install lines page-local.</td></tr>
          <tr><td>Configuration: pending · applied · failed · override</td><td class="rv-where"><a href="app/collector-config.html?id=home-laptop">Pending</a><a href="app/collector-config.html?id=shop-esp32-net">Failed</a><a href="app/collector-config.html?id=bw-pi-01">Override</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Applied on next page load (simulated check-in).</td></tr>
          <tr><td>Secrets status (Configured · Missing · Rejected)</td><td class="rv-where"><a href="app/collector-config.html?id=bw-pi-01">bw-pi-01</a><a href="app/sources.html?collector=bw-pi-01&amp;case=credentials-rejected">Rejected</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>An abandoned network draft can make the summary read Missing until it is removed.</td></tr>
          <tr><td>Enrollment: waiting · confirm · approved · expired · denied · offline</td><td class="rv-where"><a href="app/enroll.html?enroll=waiting">Waiting</a><a href="app/enroll.html?enroll=expired">Expired</a><a href="app/enroll.html?enroll=denied">Denied</a><a href="app/enroll.html?enroll=offline">Offline</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Expired, denied and offline are display-only; they don’t change the store.</td></tr>
          <tr><td>Enrollment: revoked · re-enroll</td><td class="rv-where"><a href="app/enroll.html?enroll=revoked&amp;scenario=revoked">Revoked</a><a href="app/enroll.html?enroll=reenroll&amp;code=KQPT-RNXV&amp;scenario=revoked">Re-enroll</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td><code>enroll --reenroll</code>.</td></tr>
          <tr><td>Auth expiration</td><td class="rv-where"><a href="app/sign-in.html?auth=expired-session&amp;return=incident.html%3Fid%3DINC-0412">Session expired</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Sign-in states</td><td class="rv-where"><a href="app/sign-in.html?auth=signed-out">Signed out</a><a href="app/sign-in.html?auth=redirecting">Redirecting</a><a href="app/sign-in.html?auth=callback">Callback</a><a href="app/sign-in.html?auth=access-denied">Denied</a><a href="app/sign-in.html?auth=error">Error</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td><code>?auth=</code> no longer collides with the review bar.</td></tr>
          <tr><td>Revoked credentials</td><td class="rv-where"><a href="app/collector.html?id=bw-pi-01&amp;scenario=revoked">Collector</a><a href="cli/examples/doctor-revoked.80.txt">CLI doctor</a><a href="cli/examples/error-enrollment-revoked.json">JSON error</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Maintenance</td><td class="rv-where"><a href="app/maintenance.html">Windows</a><a href="app/maintenance.html?new=1&amp;site=home">New · Home</a><a href="app/notifications.html?status=suppressed">Suppressed</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Recovery ≠ acknowledged ≠ resolved</td><td class="rv-where"><a href="app/incidents.html?tab=recovered">Recovered tab</a><a href="app/incidents.html?tab=resolved">Resolved tab</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Empty results</td><td class="rv-where"><a href="app/incidents.html?sev=info&amp;site=home">Incidents</a><a href="app/history.html?site=home&amp;from=10:00&amp;to=11:00">History</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Notification failure</td><td class="rv-where"><a href="app/notifications.html?status=failed">Failed</a><a href="app/notifications.html?status=retrying">Retrying</a><a href="app/incidents.html?nfail=1">Incidents</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Configuration validation</td><td class="rv-where"><a href="app/alert-rules.html?rule=rule-low-runtime&amp;threshold=0">Alert rule</a><a href="app/maintenance.html?new=1&amp;date=2026-10-02&amp;start=10:00&amp;end=09:00">Maintenance</a><a href="app/site-setup.html?mode=new">Site</a><a href="app/history.html?from=16:00&amp;to=17:00">Range</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Source cases</td><td class="rv-where"><a href="app/sources.html?collector=bw-pi-01">Live</a><a href="app/sources.html?case=none-found">None found</a><a href="app/sources.html?case=comms-failing">Comms failing</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>Renamed cases: live, credentials-rejected, unreachable (old names still work).</td></tr>
          <tr><td>Sync cases + journey</td><td class="rv-where"><a href="app/sync.html?case=draining">Cases</a><a href="app/sync.html?step=1#journey">Journey</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Relationships and coverage (site filter, focus)</td><td class="rv-where"><a href="app/relationships.html?focus=site:shop">Focus Shop</a><a href="app/relationships.html?cov=missing">Missing</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>ESP32 values labelled illustrative</td><td class="rv-where"><a href="app/collector.html?id=shop-esp32-net">shop-esp32-net</a><a href="app/collector-config.html?id=home-esp32-garage">home-esp32-garage</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>“Illustrative — validated board/UPS list pending”.</td></tr>
          <tr><td>Equipment table</td><td class="rv-where"><a href="app/equipment.html">Equipment</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Store integration exercised only with a stub harness.</td></tr>
          <tr><td>Mobile alert-acknowledge</td><td class="rv-where"><a href="app/incident.html?id=INC-0413">INC-0413 at ≤ 767 px</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Action bar in source; not checked at 375 px. Dense list rows at 375 px are a risk.</td></tr>
          <tr><td>Tablet layout (768–1023)</td><td class="rv-where"><a href="app/index.html">Any page</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Not reviewed separately.</td></tr>
          <tr><td>Dark / light themes</td><td class="rv-where"><a href="app/index.html?theme=dark">Dark</a><a href="app/index.html?theme=light">Light</a><a href="app/components.html">Gallery</a></td><td><span class="rv-status rv-status--partial">Partial</span></td><td>Contrast calculated, not visually checked.</td></tr>
          <tr><td>Calm state</td><td class="rv-where"><a href="app/incidents.html?state=calm">Incidents</a><a href="app/collectors.html?state=calm">Collectors</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Availability with unknown and maintenance</td><td class="rv-where"><a href="app/site.html?id=home">Home</a><a href="app/history.html?site=home">History</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Keyboard (shortcuts, Esc, focus)</td><td class="rv-where"><a href="app/equipment.html">Equipment inspector</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td>In source; not tested with assistive technology.</td></tr>
          <tr><td>CLI dark · light · NO_COLOR at 80 / 120</td><td class="rv-where"><a href="cli/index.html?w=80&amp;term=dark">80 dark</a><a href="cli/index.html?w=120&amp;term=light">120 light</a><a href="cli/index.html?term=nocolor">NO_COLOR</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>CLI grammar, elevation, exit codes</td><td class="rv-where"><a href="cli/index.html#elevation">Elevation</a><a href="cli/index.html#exit">Exit codes</a><a href="cli/examples/sources-test-unelevated.80.txt">Unelevated</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td><code>status</code> exits 0 when the query succeeds.</td></tr>
          <tr><td>CLI JSON</td><td class="rv-where"><a href="cli/index.html#json-schema">Schema</a><a href="cli/examples/status-studio-mbp.json">status</a><a href="cli/examples/queue-lab-pi-stalled.json">queue</a></td><td><span class="rv-status rv-status--designed">Designed</span></td><td></td></tr>
          <tr><td>Rendered screenshots / visual inspection</td><td><a href="#visual">Visual checklist</a></td><td><span class="rv-status rv-status--not">Not designed</span></td><td>Not captured. Browser testing was skipped for 0.1.12 at your request; the earlier launch failure remains documented in 0.1.11 evidence.</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- f. Unverified visual checklist -->
  <section class="rv-sec" id="visual" aria-labelledby="h-vis">
    <p class="mk-eyebrow">f · Visual checklist · UNVERIFIED</p>
    <h2 id="h-vis">UNVERIFIED visual checklist</h2>
    <p class="rv-note"><strong>Every item below is “Not verified (skipped by request for 0.1.12)”.</strong> Nothing has been opened in a browser or on a device. Use this as the checklist for the first visual review: open each journey’s routes at each width in both themes and record defects.</p>
    <div class="mk-table-wrap" style="--mk-table-max-h:none" tabindex="0" role="region" aria-label="Unverified visual checklist by journey, width and theme">
      <table class="mk-table rv-table rv-vis" style="--mk-table-min:1100px">
        <caption class="mk-visually-hidden">Visual checklist: journey by viewport width and theme. All cells not verified.</caption>
        <thead><tr><th scope="col">Journey</th><th scope="col">375 px · dark</th><th scope="col">375 px · light</th><th scope="col">768 px · dark</th><th scope="col">768 px · light</th><th scope="col">1024 px · dark</th><th scope="col">1024 px · light</th><th scope="col">1440 px · dark</th><th scope="col">1440 px · light</th></tr></thead>
        <tbody>
          <tr><th scope="row"><a href="#j1">1 · Empty account → first readings → fleet</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j2">2 · On-battery → acknowledgment → restoration</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j3">3 · Contact lost → backfill (Duplex)</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j4">4 · UPS source stops, collector online</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j5">5 · Credential revoked → re-enrollment</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j6">6 · Network UPS setup (5 outcomes)</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j7">7 · Witness down vs contact lost vs local probe</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j8">8 · Workstation sleep</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j9">9 · Equipment-filtered history · 7 d</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#corrections">0.1.7 corrections (identity, gating, witness-down, ranges, freshness, platforms)</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><th scope="row"><a href="#j10">10 · Mobile incident acknowledge</a></th><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
        </tbody>
      </table>
    </div>
    <h3>Cross-cutting checks</h3>
    <div class="mk-table-wrap" style="--mk-table-max-h:none" tabindex="0" role="region" aria-label="Unverified cross-cutting checks">
      <table class="mk-table rv-table" style="--mk-table-min:640px">
        <caption class="mk-visually-hidden">Cross-cutting visual and interaction checks, all not verified</caption>
        <thead><tr><th scope="col">Check</th><th scope="col">What to look for</th><th scope="col">Status</th></tr></thead>
        <tbody>
          <tr><td>Keyboard navigation</td><td>Tab order, skip link, shortcuts, review bar, dataset switch, Reset demo</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Focus</td><td>Visible focus ring in both themes; focus returns after dialogs, inspector and bottom sheet</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Overflow</td><td>No page-level horizontal scroll at 375/768/1024/1440; tables scroll inside their wrappers; long ids wrap</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Chart labels</td><td>Axis and band labels legible; “No records” hatch, coverage bands, Site context; 7 d aggregates at 375 px</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Dialogs</td><td>Reset demo, acknowledge, network hard-duplicate card and serial-match confirm; Esc closes; focus trapped and restored</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Forms</td><td>Site, network source endpoint, alert rule, maintenance; validation messages next to fields; 44 px targets on mobile</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Terminal readability</td><td>Onboarding command blocks and CLI mockups at 80/120 columns in dark, light and NO_COLOR</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Click and submit wiring</td><td>Every 0.1.7 button (Simulate reconnect/check-in/disconnect, Test, Identify, Use, confirm/cancel, role switch save) does what correction cards 1–7 describe</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Reduced motion</td><td>No animation with prefers-reduced-motion</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
          <tr><td>Contrast in place</td><td>Status colours and muted text on real surfaces in both themes</td><td><span class="rv-nv">Not verified</span>(skipped by request for 0.1.12)</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- g. Screenshots -->
  <section class="rv-sec" id="screenshots" aria-labelledby="h-shots">
    <p class="mk-eyebrow">g · Screenshots</p>
    <h2 id="h-shots">Requested screenshots</h2>
    <p class="rv-note">None captured in 0.1.12: browser tests and screenshots were skipped at your request. Earlier checkpoints also contained no screenshots; the 0.1.11 launch failure is preserved in its historical record. Exact routes for manual capture are below. Reset demo first, and walk journey 1 before the first-run shot.</p>
    <div class="mk-table-wrap" style="--mk-table-max-h:none" tabindex="0" role="region" aria-label="Screenshot list">
      <table class="mk-table rv-table" style="--mk-table-min:720px">
        <caption class="mk-visually-hidden">Requested screenshots, route to capture and status</caption>
        <thead><tr><th scope="col">Screenshot</th><th scope="col">Route to capture</th><th scope="col">Status</th></tr></thead>
        <tbody>
          <tr><td>Fleet overview</td><td class="rv-where"><a href="app/index.html?state=incident">app/index.html?state=incident</a><a href="app/index.html?state=incident&amp;theme=light">light</a></td><td>Not captured</td></tr>
          <tr><td>First-run enrollment</td><td class="rv-where"><a href="app/enroll.html?scenario=empty&amp;enroll=confirm&amp;code=BKTN-XRWP&amp;from=welcome">app/enroll.html?scenario=empty&amp;enroll=confirm&amp;code=BKTN-XRWP&amp;from=welcome</a><span>(after welcome steps 1–5)</span></td><td>Not captured</td></tr>
          <tr><td>Network source setup</td><td class="rv-where"><a href="app/network-source.html?collector=bw-pi-01">app/network-source.html?collector=bw-pi-01</a><span>(NUT 192.168.1.20 · rackups; also the four failure outcomes)</span></td><td>Not captured</td></tr>
          <tr><td>Incident acknowledgment and recovery</td><td class="rv-where"><a href="app/incident.html?id=INC-0412&amp;step=ack">app/incident.html?id=INC-0412&amp;step=ack</a><a href="app/incident.html?id=INC-0412&amp;step=restored">…&amp;step=restored</a></td><td>Not captured</td></tr>
          <tr><td>Equipment-filtered history</td><td class="rv-where"><a href="app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h">app/history.html?site=broadwater&amp;ups=bw-office-ups&amp;range=24h</a><a href="app/history.html?site=shop&amp;ups=shop-bench-ups&amp;range=7d">7 d</a></td><td>Not captured</td></tr>
          <tr><td>Mobile incident detail</td><td class="rv-where"><a href="app/incident.html?id=INC-0413">app/incident.html?id=INC-0413</a><span>(375 px viewport)</span></td><td>Not captured</td></tr>
          <tr><td>Network identity · hard duplicate and serial-match confirm</td><td class="rv-where"><a href="app/network-source.html?collector=bw-pi-01">app/network-source.html?collector=bw-pi-01</a><span>(SNMP 10.0.20.15 blocked; 10.0.20.16 confirm)</span></td><td>Not captured</td></tr>
          <tr><td>Queued test and waiting states</td><td class="rv-where"><a href="app/network-source.html?collector=home-laptop">app/network-source.html?collector=home-laptop</a><a href="app/collector-config.html?id=home-laptop">configuration (pending)</a></td><td>Not captured</td></tr>
          <tr><td>Witness-down history (unknown after 15:04:12)</td><td class="rv-where"><a href="app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22">app/history.html?site=home&amp;ups=office-desk-ups&amp;scenario=witness-down&amp;from=14:30&amp;to=15:22</a></td><td>Not captured</td></tr>
          <tr><td>Freshness at 600 s cadence</td><td class="rv-where"><a href="app/ups.html?id=bw-office-ups">app/ups.html?id=bw-office-ups</a></td><td>Not captured</td></tr>
          <tr><td>Windows and ESP32 instructions</td><td class="rv-where"><a href="app/install.html?platform=windows">install · Windows</a><a href="app/install.html?platform=esp32">install · ESP32</a></td><td>Not captured</td></tr>
          <tr><td>Corrected history · group 9 A and B</td><td class="rv-where"><a href="#c110-a">30-second gap setup</a><a href="#c110-b">Two-gap setup</a><a href="#c0111">15:28:30–15:28:40 and 15:25:15–15:30:15 exact ranges</a></td><td>Not captured — skipped by request</td></tr>
          <tr><td>CLI status and enrollment</td><td class="rv-where"><a href="cli/index.html?w=80#g-status">cli/index.html?w=80#g-status</a><a href="cli/index.html?w=80#g-enroll">#g-enroll</a><a href="cli/examples/status-bw.80.txt">status-bw.80.txt</a><a href="cli/examples/enroll-headless.80.txt">enroll-headless.80.txt</a></td><td>Not captured</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- h. CLI examples -->
  <section class="rv-sec" id="cli-examples" aria-labelledby="h-cliex">
    <p class="mk-eyebrow">h · CLI examples</p>
    <h2 id="h-cliex">CLI examples by category (151 files)</h2>
    <p class="rv-note">Generated from one source (<code>cli/cli-screens.js</code>). Added in 0.1.7 (15 screens × 80/120 col = 30 files): <code>config-set-windows-unelevated</code>, <code>config-validate-macos</code>, <code>config-validate-windows</code>, <code>help-esp32</code>, <code>logs-macos</code>, <code>logs-windows</code>, <code>roles-show-pending</code>, <code>sources-add-deskups</code>, <code>sources-add-rackups</code>, <code>sources-add-studio</code>, <code>sources-local-access-fedora</code>, <code>sources-local-access-macos</code>, <code>sources-test-offline</code>, <code>sources-test-serial-match</code>, <code>sources-use-waiting</code>. Renamed in 0.1.6: <code>error-credential-revoked.json</code> → <code>error-enrollment-revoked.json</code>; <code>sources-auth.*</code> → <code>sources-access-rejected.*</code>; <code>roles-perm.*</code> → <code>sources-test-unelevated.*</code>; <code>sources-partial.*</code> → <code>sources-show.*</code>. All files are in the checkpoint archive.</p>
    <div class="rv-grid">
      <article class="rv-card"><h3>mainskeeper (bare) (2)</h3><ul class="rv-links"><li><span><code>bare</code> · <a href="cli/examples/bare.80.txt">80 col</a> · <a href="cli/examples/bare.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>help (6)</h3><ul class="rv-links"><li><span><code>help</code> · <a href="cli/examples/help.80.txt">80 col</a> · <a href="cli/examples/help.120.txt">120 col</a></span></li><li><span><code>help-esp32</code> · <a href="cli/examples/help-esp32.80.txt">80 col</a> · <a href="cli/examples/help-esp32.120.txt">120 col</a></span></li><li><span><code>help-queue</code> · <a href="cli/examples/help-queue.80.txt">80 col</a> · <a href="cli/examples/help-queue.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>Usage error (2)</h3><ul class="rv-links"><li><span><code>usage-error</code> · <a href="cli/examples/usage-error.80.txt">80 col</a> · <a href="cli/examples/usage-error.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>version (2)</h3><ul class="rv-links"><li><span><code>version</code> · <a href="cli/examples/version.80.txt">80 col</a> · <a href="cli/examples/version.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>enroll (16)</h3><ul class="rv-links"><li><span><code>enroll-approved</code> · <a href="cli/examples/enroll-approved.80.txt">80 col</a> · <a href="cli/examples/enroll-approved.120.txt">120 col</a></span></li><li><span><code>enroll-denied</code> · <a href="cli/examples/enroll-denied.80.txt">80 col</a> · <a href="cli/examples/enroll-denied.120.txt">120 col</a></span></li><li><span><code>enroll-desktop</code> · <a href="cli/examples/enroll-desktop.80.txt">80 col</a> · <a href="cli/examples/enroll-desktop.120.txt">120 col</a></span></li><li><span><code>enroll-expired</code> · <a href="cli/examples/enroll-expired.80.txt">80 col</a> · <a href="cli/examples/enroll-expired.120.txt">120 col</a></span></li><li><span><code>enroll-headless</code> · <a href="cli/examples/enroll-headless.80.txt">80 col</a> · <a href="cli/examples/enroll-headless.120.txt">120 col</a></span></li><li><span><code>enroll-offline</code> · <a href="cli/examples/enroll-offline.80.txt">80 col</a> · <a href="cli/examples/enroll-offline.120.txt">120 col</a></span></li><li><span><code>enroll-reenroll</code> · <a href="cli/examples/enroll-reenroll.80.txt">80 col</a> · <a href="cli/examples/enroll-reenroll.120.txt">120 col</a></span></li><li><span><code>enroll-waiting</code> · <a href="cli/examples/enroll-waiting.80.txt">80 col</a> · <a href="cli/examples/enroll-waiting.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>status (11)</h3><ul class="rv-links"><li><span><code>status-bw</code> · <a href="cli/examples/status-bw.80.txt">80 col</a> · <a href="cli/examples/status-bw.120.txt">120 col</a></span></li><li><span><code>status-bw-pi-01</code> · <a href="cli/examples/status-bw-pi-01.json">JSON</a></span></li><li><span><code>status-duplex</code> · <a href="cli/examples/status-duplex.80.txt">80 col</a> · <a href="cli/examples/status-duplex.120.txt">120 col</a></span></li><li><span><code>status-duplex-pi-probe</code> · <a href="cli/examples/status-duplex-pi-probe.json">JSON</a></span></li><li><span><code>status-studio</code> · <a href="cli/examples/status-studio.80.txt">80 col</a> · <a href="cli/examples/status-studio.120.txt">120 col</a></span></li><li><span><code>status-studio-mbp</code> · <a href="cli/examples/status-studio-mbp.json">JSON</a></span></li><li><span><code>status-watch</code> · <a href="cli/examples/status-watch.80.txt">80 col</a> · <a href="cli/examples/status-watch.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>sources (46)</h3><ul class="rv-links"><li><span><code>sources-access-rejected</code> · <a href="cli/examples/sources-access-rejected.80.txt">80 col</a> · <a href="cli/examples/sources-access-rejected.120.txt">120 col</a></span></li><li><span><code>sources-add-deskups</code> · <a href="cli/examples/sources-add-deskups.80.txt">80 col</a> · <a href="cli/examples/sources-add-deskups.120.txt">120 col</a></span></li><li><span><code>sources-add-duplicate</code> · <a href="cli/examples/sources-add-duplicate.80.txt">80 col</a> · <a href="cli/examples/sources-add-duplicate.120.txt">120 col</a></span></li><li><span><code>sources-add-nut</code> · <a href="cli/examples/sources-add-nut.80.txt">80 col</a> · <a href="cli/examples/sources-add-nut.120.txt">120 col</a></span></li><li><span><code>sources-add-rackups</code> · <a href="cli/examples/sources-add-rackups.80.txt">80 col</a> · <a href="cli/examples/sources-add-rackups.120.txt">120 col</a></span></li><li><span><code>sources-add-studio</code> · <a href="cli/examples/sources-add-studio.80.txt">80 col</a> · <a href="cli/examples/sources-add-studio.120.txt">120 col</a></span></li><li><span><code>sources-add-unreachable</code> · <a href="cli/examples/sources-add-unreachable.80.txt">80 col</a> · <a href="cli/examples/sources-add-unreachable.120.txt">120 col</a></span></li><li><span><code>sources-existing</code> · <a href="cli/examples/sources-existing.80.txt">80 col</a> · <a href="cli/examples/sources-existing.120.txt">120 col</a></span></li><li><span><code>sources-list</code> · <a href="cli/examples/sources-list.80.txt">80 col</a> · <a href="cli/examples/sources-list.120.txt">120 col</a></span></li><li><span><code>sources-local-access-fedora</code> · <a href="cli/examples/sources-local-access-fedora.80.txt">80 col</a> · <a href="cli/examples/sources-local-access-fedora.120.txt">120 col</a></span></li><li><span><code>sources-local-access-macos</code> · <a href="cli/examples/sources-local-access-macos.80.txt">80 col</a> · <a href="cli/examples/sources-local-access-macos.120.txt">120 col</a></span></li><li><span><code>sources-local-access-prompt</code> · <a href="cli/examples/sources-local-access-prompt.80.txt">80 col</a> · <a href="cli/examples/sources-local-access-prompt.120.txt">120 col</a></span></li><li><span><code>sources-multi</code> · <a href="cli/examples/sources-multi.80.txt">80 col</a> · <a href="cli/examples/sources-multi.120.txt">120 col</a></span></li><li><span><code>sources-none</code> · <a href="cli/examples/sources-none.80.txt">80 col</a> · <a href="cli/examples/sources-none.120.txt">120 col</a></span></li><li><span><code>sources-owned</code> · <a href="cli/examples/sources-owned.80.txt">80 col</a> · <a href="cli/examples/sources-owned.120.txt">120 col</a></span></li><li><span><code>sources-show</code> · <a href="cli/examples/sources-show.80.txt">80 col</a> · <a href="cli/examples/sources-show.120.txt">120 col</a></span></li><li><span><code>sources-test-offline</code> · <a href="cli/examples/sources-test-offline.80.txt">80 col</a> · <a href="cli/examples/sources-test-offline.120.txt">120 col</a></span></li><li><span><code>sources-test-ok</code> · <a href="cli/examples/sources-test-ok.80.txt">80 col</a> · <a href="cli/examples/sources-test-ok.120.txt">120 col</a></span></li><li><span><code>sources-test-partial</code> · <a href="cli/examples/sources-test-partial.80.txt">80 col</a> · <a href="cli/examples/sources-test-partial.120.txt">120 col</a></span></li><li><span><code>sources-test-serial-match</code> · <a href="cli/examples/sources-test-serial-match.80.txt">80 col</a> · <a href="cli/examples/sources-test-serial-match.120.txt">120 col</a></span></li><li><span><code>sources-test-unelevated</code> · <a href="cli/examples/sources-test-unelevated.80.txt">80 col</a> · <a href="cli/examples/sources-test-unelevated.120.txt">120 col</a></span></li><li><span><code>sources-use</code> · <a href="cli/examples/sources-use.80.txt">80 col</a> · <a href="cli/examples/sources-use.120.txt">120 col</a></span></li><li><span><code>sources-use-waiting</code> · <a href="cli/examples/sources-use-waiting.80.txt">80 col</a> · <a href="cli/examples/sources-use-waiting.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>roles (6)</h3><ul class="rv-links"><li><span><code>roles-set</code> · <a href="cli/examples/roles-set.80.txt">80 col</a> · <a href="cli/examples/roles-set.120.txt">120 col</a></span></li><li><span><code>roles-show</code> · <a href="cli/examples/roles-show.80.txt">80 col</a> · <a href="cli/examples/roles-show.120.txt">120 col</a></span></li><li><span><code>roles-show-pending</code> · <a href="cli/examples/roles-show-pending.80.txt">80 col</a> · <a href="cli/examples/roles-show-pending.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>config (20)</h3><ul class="rv-links"><li><span><code>config-set-local</code> · <a href="cli/examples/config-set-local.80.txt">80 col</a> · <a href="cli/examples/config-set-local.120.txt">120 col</a></span></li><li><span><code>config-set-managed</code> · <a href="cli/examples/config-set-managed.80.txt">80 col</a> · <a href="cli/examples/config-set-managed.120.txt">120 col</a></span></li><li><span><code>config-set-override</code> · <a href="cli/examples/config-set-override.80.txt">80 col</a> · <a href="cli/examples/config-set-override.120.txt">120 col</a></span></li><li><span><code>config-set-windows</code> · <a href="cli/examples/config-set-windows.80.txt">80 col</a> · <a href="cli/examples/config-set-windows.120.txt">120 col</a></span></li><li><span><code>config-set-windows-unelevated</code> · <a href="cli/examples/config-set-windows-unelevated.80.txt">80 col</a> · <a href="cli/examples/config-set-windows-unelevated.120.txt">120 col</a></span></li><li><span><code>config-show</code> · <a href="cli/examples/config-show.80.txt">80 col</a> · <a href="cli/examples/config-show.120.txt">120 col</a></span></li><li><span><code>config-show-effective</code> · <a href="cli/examples/config-show-effective.80.txt">80 col</a> · <a href="cli/examples/config-show-effective.120.txt">120 col</a></span></li><li><span><code>config-validate</code> · <a href="cli/examples/config-validate.80.txt">80 col</a> · <a href="cli/examples/config-validate.120.txt">120 col</a></span></li><li><span><code>config-validate-macos</code> · <a href="cli/examples/config-validate-macos.80.txt">80 col</a> · <a href="cli/examples/config-validate-macos.120.txt">120 col</a></span></li><li><span><code>config-validate-windows</code> · <a href="cli/examples/config-validate-windows.80.txt">80 col</a> · <a href="cli/examples/config-validate-windows.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>queue (11)</h3><ul class="rv-links"><li><span><code>queue-draining</code> · <a href="cli/examples/queue-draining.80.txt">80 col</a> · <a href="cli/examples/queue-draining.120.txt">120 col</a></span></li><li><span><code>queue-dropped</code> · <a href="cli/examples/queue-dropped.80.txt">80 col</a> · <a href="cli/examples/queue-dropped.120.txt">120 col</a></span></li><li><span><code>queue-duplex-pi-probe</code> · <a href="cli/examples/queue-duplex-pi-probe.json">JSON</a></span></li><li><span><code>queue-lab-pi-dropped</code> · <a href="cli/examples/queue-lab-pi-dropped.json">JSON</a></span></li><li><span><code>queue-lab-pi-stalled</code> · <a href="cli/examples/queue-lab-pi-stalled.json">JSON</a></span></li><li><span><code>queue-pressure</code> · <a href="cli/examples/queue-pressure.80.txt">80 col</a> · <a href="cli/examples/queue-pressure.120.txt">120 col</a></span></li><li><span><code>queue-stalled</code> · <a href="cli/examples/queue-stalled.80.txt">80 col</a> · <a href="cli/examples/queue-stalled.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>doctor (14)</h3><ul class="rv-links"><li><span><code>doctor-cloud</code> · <a href="cli/examples/doctor-cloud.80.txt">80 col</a> · <a href="cli/examples/doctor-cloud.120.txt">120 col</a></span></li><li><span><code>doctor-comms</code> · <a href="cli/examples/doctor-comms.80.txt">80 col</a> · <a href="cli/examples/doctor-comms.120.txt">120 col</a></span></li><li><span><code>doctor-hw</code> · <a href="cli/examples/doctor-hw.80.txt">80 col</a> · <a href="cli/examples/doctor-hw.120.txt">120 col</a></span></li><li><span><code>doctor-ok</code> · <a href="cli/examples/doctor-ok.80.txt">80 col</a> · <a href="cli/examples/doctor-ok.120.txt">120 col</a></span></li><li><span><code>doctor-perm</code> · <a href="cli/examples/doctor-perm.80.txt">80 col</a> · <a href="cli/examples/doctor-perm.120.txt">120 col</a></span></li><li><span><code>doctor-revoked</code> · <a href="cli/examples/doctor-revoked.80.txt">80 col</a> · <a href="cli/examples/doctor-revoked.120.txt">120 col</a></span></li><li><span><code>doctor-service</code> · <a href="cli/examples/doctor-service.80.txt">80 col</a> · <a href="cli/examples/doctor-service.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>service (6)</h3><ul class="rv-links"><li><span><code>service-launchd</code> · <a href="cli/examples/service-launchd.80.txt">80 col</a> · <a href="cli/examples/service-launchd.120.txt">120 col</a></span></li><li><span><code>service-systemd</code> · <a href="cli/examples/service-systemd.80.txt">80 col</a> · <a href="cli/examples/service-systemd.120.txt">120 col</a></span></li><li><span><code>service-windows</code> · <a href="cli/examples/service-windows.80.txt">80 col</a> · <a href="cli/examples/service-windows.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>logs (8)</h3><ul class="rv-links"><li><span><code>logs</code> · <a href="cli/examples/logs.80.txt">80 col</a> · <a href="cli/examples/logs.120.txt">120 col</a></span></li><li><span><code>logs-group</code> · <a href="cli/examples/logs-group.80.txt">80 col</a> · <a href="cli/examples/logs-group.120.txt">120 col</a></span></li><li><span><code>logs-macos</code> · <a href="cli/examples/logs-macos.80.txt">80 col</a> · <a href="cli/examples/logs-macos.120.txt">120 col</a></span></li><li><span><code>logs-windows</code> · <a href="cli/examples/logs-windows.80.txt">80 col</a> · <a href="cli/examples/logs-windows.120.txt">120 col</a></span></li></ul></article>
      <article class="rv-card"><h3>JSON errors (1)</h3><ul class="rv-links"><li><span><code>error-enrollment-revoked</code> · <a href="cli/examples/error-enrollment-revoked.json">JSON</a></span></li></ul></article>
    </div>
  </section>

  <!-- e. Design system -->
  <section class="rv-sec" id="system" aria-labelledby="h-ds">
    <p class="mk-eyebrow">i · Design system</p>
    <h2 id="h-ds">Design-system summary</h2>
    <div class="rv-grid">
      <article class="rv-card">
        <h3>Colour roles (current theme)</h3>
        <div class="rv-swatches">
          <div class="rv-sw"><i style="background:var(--mk-bg)"></i><span>Ground<code>--mk-bg · #101418 / #F4F6F8</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-surface-1)"></i><span>Surface<code>--mk-surface-1</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-text)"></i><span>Text<code>--mk-text · #E9EEF2 / #101418</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-brand-mint)"></i><span>Mint — live, observed, OK, links<code>#7FE0C4 / #0B7358</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-brand-sodium)"></i><span>Sodium — brand, selection, focus only<code>#FF9F43 / #A34D00</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-critical)"></i><span>Critical — on battery<code>#FF6B6B / #C02F2F</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-warning)"></i><span>Warning — contact lost<code>#E8D85C / #6E6100</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-unknown)"></i><span>Unknown, last known, unsupported<code>#A3B0BC / #4F5C69</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-connectivity)"></i><span>Connectivity — local probe evidence<code>#5CB8F5 / #1D66A8</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-sync)"></i><span>Sync — pending, replayed<code>#A99CF7 / #5747C2</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-maintenance)"></i><span>Maintenance<code>#B4BFCA / #56626E</code></span></div>
          <div class="rv-sw"><i style="background:var(--mk-power-line)"></i><span>“Powers” lines<code>#C5CFD8 / #56626E</code></span></div>
        </div>
        <p class="rv-note">Dark is the default; light uses <code>[data-theme="light"]</code>. Every text role is ≥4.5:1 on bg, surface-1 and surface-2 in both themes, calculated in CONTRACT §3.2. Warning yellow differs from sodium by 24–25° of hue and uses a different glyph. Type: Sora (UI), DM Mono with tabular numerals (data). Body 16 px, dense cells 14 px, minimum 12 px.</p>
      </article>
      <article class="rv-card">
        <h3>Status glyph conventions</h3>
        <p class="rv-note">Status is always glyph + word. Colour supports the meaning but never carries it alone.</p>
        <dl class="rv-dl" id="rv-glyphs">
          <div><dt data-badge="on-battery">On battery</dt><dd>Critical. Battery glyph.</dd></div>
          <div><dt data-badge="on-utility">On utility</dt><dd>OK. Filled circle.</dd></div>
          <div><dt data-badge="contact-lost">Contact lost</dt><dd>Warning. Dashed circle with “?”. Server-observed, cause unknown.</dd></div>
          <div><dt data-badge="connectivity-fail">Connectivity failure</dt><dd>Connectivity blue. Bars with ×. Confirmed by local probe.</dd></div>
          <div><dt data-badge="comms-lost">UPS comms lost</dt><dd>Warning. The collector still reports.</dd></div>
          <div><dt data-badge="last-known">Last known</dt><dd>Dashed hollow circle, always with time and age. Stale beyond 2 × the reporting interval + allowance, or when contact is lost.</dd></div>
          <div><dt data-badge="unsupported">Unsupported</dt><dd>Hatched dash. Never 0.</dd></div>
          <div><dt data-badge="replayed">Received late · replayed</dt><dd>Sync violet. Received more than 2 min after observation while reconnecting. Backfill does not send duplicate or obsolete notifications.</dd></div>
          <div><dt data-badge="recovered">Recovered</dt><dd>Outlined check.</dd></div>
          <div><dt data-badge="maintenance">Maintenance</dt><dd>Wrench, hatched band.</dd></div>
          <div><dt data-badge="not-acknowledged">Not acknowledged</dt><dd>Neutral, dashed. Acknowledgment changes nothing on equipment.</dd></div>
          <div><dt data-badge="selected">Selected</dt><dd>Sodium ring plus tint. Never a status.</dd></div>
        </dl>
      </article>
      <article class="rv-card">
        <h3>Chart conventions</h3>
        <dl class="rv-dl">
          <div><dt>Measured</dt><dd>Solid line, mint.</dd></div>
          <div><dt>Estimate</dt><dd>Dashed line, labelled “est.” (UPS estimate).</dd></div>
          <div><dt>Received late</dt><dd>Hollow marker plus received time.</dd></div>
          <div><dt>No records</dt><dd>Hatched band. Never interpolated, never drawn as zero.</dd></div>
          <div><dt>History arriving</dt><dd>Periwinkle hatch: records exist on the collector but are not yet uploaded.</dd></div>
          <div><dt>Uncertain</dt><dd>Bracket with ±seconds.</dd></div>
          <div><dt>Maintenance / battery</dt><dd>Background bands.</dd></div>
          <div><dt>Live edge</dt><dd>“Now” marker at 15:22.</dd></div>
        </dl>
      </article>
      <article class="rv-card">
        <h3>Coverage model</h3>
        <dl class="rv-dl">
          <div><dt>Directly monitored</dt><dd>A collector reads it now.</dd></div>
          <div><dt>Relationship recorded — no direct monitoring expected</dt><dd>For example, a sleeping workstation with no agent behind a monitored UPS.</dd></div>
          <div><dt>Measurement unsupported</dt><dd>The UPS or source cannot report this value.</dd></div>
          <div><dt>Expected collector/source missing</dt><dd>Something that should report is not reporting.</dd></div>
        </dl>
        <p class="rv-note">Links are <em>reads</em> (collector → UPS), <em>powers</em> (UPS → equipment) and <em>checks</em> (collector → target). All are marked “configured”, not measured.</p>
      </article>
      <article class="rv-card">
        <h3>Wording glossary</h3>
        <dl class="rv-dl">
          <div><dt>Contact lost — cause unknown</dt><dd>Server stopped hearing; no local evidence.</dd></div>
          <div><dt>Connectivity failure confirmed by local probe</dt><dd>Probe records show targets failing.</dd></div>
          <div><dt>On schedule · Overdue · Stale · last known 15:10</dt><dd>Relative to the reporting interval I with allowance g = max(30 s, I/2): ≤ I + g · ≤ 2I + g · beyond. Always with “read N ago · every I”.</dd></div>
          <div><dt>No records — collector not reporting</dt><dd>Unknown, never filled, joined or counted as up or down.</dd></div>
          <div><dt>Configured — waiting for UPS role / for collector</dt><dd>A saved network source that cannot be read yet.</dd></div>
          <div><dt>Possibly the same UPS as … (serial match) — confirm</dt><dd>A trustworthy serial match at a different identity. A question, never a block.</dd></div>
          <div><dt>Unsupported / Not reported</dt><dd>Cannot report / supported but missing.</dd></div>
          <div><dt>UPS estimate (est.)</dt><dd>A prediction, not “runtime remaining”.</dd></div>
          <div><dt>Observed on battery</dt><dd>Measured between observed transitions.</dd></div>
          <div><dt>At least 27 minutes on battery; endpoint unknown</dt><dd>No observed end.</dd></div>
          <div><dt>Received late · replayed</dt><dd>Received more than 2 min after observation while the collector reconnected from its buffer. Normal latency is not replay.</dd></div>
          <div><dt>History arriving / No records / No records received</dt><dd>Pending upload / none exist / nothing since last contact.</dd></div>
          <div><dt>Site witness</dt><dd>Always-on collector that carries site expectations.</dd></div>
          <div><dt>Asleep (ordinary workstation) — no incident</dt><dd>A workstation that is not a witness; excluded from expected counts.</dd></div>
          <div><dt>Site still reachable: home-esp32-garage reporting</dt><dd>A witness is down but another collector at the site still reports.</dd></div>
          <div><dt>First evidence · Grace period started · Declared · Acknowledged · Recovered · Resolved</dt><dd>Incident timestamps. Resolved = recovered and stable for 5 min.</dd></div>
        </dl>
        <p class="rv-note">Avoid “offline” for contact lost, “down”, “battery health” for charge, “0%” for missing data, and any control verbs.</p>
      </article>
    </div>
  </section>

  <!-- f. Scenario -->
  <section class="rv-sec" id="scenario" aria-labelledby="h-sc">
    <p class="mk-eyebrow">j · Scenario</p>
    <h2 id="h-sc">Scenario summary (from the fixture)</h2>
    <p class="rv-note">Now = Wed 30 Sep 2026 15:22:00 PDT. Fictional demonstration data. The counts below are read from the demo incident fixture (<code>MK.select.fleetCounts(MK.data('incident','default'))</code>) when scripts load, whatever dataset is active. Store changes such as acknowledgments still apply.</p>
    <div class="rv-counts" id="rv-counts">
      <div class="rv-count"><b data-k="collectors">4 of 6</b><span>expected collectors reporting · 1 workstation asleep</span></div>
      <div class="rv-count"><b data-k="ups">5 of 6</b><span>UPSs communicating</span></div>
      <div class="rv-count"><b data-k="onBattery">1</b><span>UPS on battery</span></div>
      <div class="rv-count"><b data-k="open">2</b><span>open incidents (1 critical, 1 warning; 0 acknowledged)</span></div>
      <div class="rv-count"><b data-k="rec">1</b><span>recovery today</span></div>
    </div>
    <div class="rv-grid">
      <article class="rv-card">
        <h3>Two open incidents + one recent recovery</h3>
        <ul class="rv-links">
          <li><a href="app/incident.html?id=INC-0412">INC-0412 · Broadwater · BW Rack UPS on battery</a><span class="rv-why">Critical · open · not acknowledged · not recovered · first evidence 15:09:00 · declared 15:10:00 · UPS estimate 18 min est. (initial 34) · email and push delivered</span></li>
          <li><a href="app/incident.html?id=INC-0413">INC-0413 · Shop · Contact lost with 2 of 2 probes — cause unknown</a><span class="rv-why">Warning · open · last contact 15:10:02 · first evidence 15:10:32 · declared 15:15:02 after the 5 min grace · email 15:15:03 · push failed 15:15:04, retrying</span></li>
          <li><a href="app/incident.html?id=INC-0411">INC-0411 · Duplex · Contact lost — recovered 15:02:06</a><span class="rv-why">First evidence 13:41:00 (local probe) · declared 13:49:00 · acknowledged 13:52 by jp · resolved 15:07:06 · records still arriving, shown separately</span></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Site summaries</h3>
        <dl class="rv-dl" id="rv-sites">
          <div><dt>Broadwater</dt><dd>1 of 2 UPSs on battery · 1 on utility (separate circuit)</dd></div>
          <div><dt>Home</dt><dd>Healthy</dd></div>
          <div><dt>Shop</dt><dd>Contact lost · last known 15:10</dd></div>
          <div><dt>Duplex</dt><dd>Recovered 15:02 · history arriving (1,248 pending)</dd></div>
        </dl>
      </article>
      <article class="rv-card">
        <h3>Duplex 14:30–14:52</h3>
        <dl class="rv-dl">
          <div><dt><span class="rv-num">13:41</span></dt><dd>Local probe: all 3 targets fail (confirmed). Received 15:02:06.</dd></div>
          <div><dt><span class="rv-num">13:44</span></dt><dd>Server: contact lost, cause unknown.</dd></div>
          <div><dt><span class="rv-num">14:03</span></dt><dd>UPS on battery, recorded locally. Received 15:17:40 · replayed (historical backfill, no duplicate or obsolete notification).</dd></div>
          <div><dt><span class="rv-num">14:30–14:52</span></dt><dd>Probe offline: no records. Connectivity unknown. Power is a gap. “At least 27 minutes on battery; endpoint unknown.”</dd></div>
          <div><dt><span class="rv-num">14:52:10</span></dt><dd>First post-gap observation: utility present, charge 41%, ±90 s until clock sync at 14:54. Restoration happened at an unknown time between 14:30 and 14:52.</dd></div>
          <div><dt><span class="rv-num">15:02:06</span></dt><dd>Fresh contact and probe evidence → INC-0411 recovered; resolved 15:07:06 after 5 min stable. Backlog keeps draining.</dd></div>
        </dl>
      </article>
    </div>
  </section>

  <!-- g. Exploration -->
  <section class="rv-sec" id="exploration" aria-labelledby="h-ex">
    <p class="mk-eyebrow">k · Preserved exploration</p>
    <h2 id="h-ex">Preserved exploration (unchanged references)</h2>
    <p class="rv-note">Rounds 1–3 explored nine directions. They are kept unchanged for reference. This design takes its identity from concept 8 and its equipment workflows from concept 7; see the <a href="notes.html">notes</a> for provenance.</p>
    <div class="rv-grid">
      <article class="rv-card">
        <h3>Comparison canvases and notes</h3>
        <ul class="rv-links">
          <li><a href="../review/round-3.html">Round 3 comparison canvas (concepts 7–9)</a></li>
          <li><a href="../notes/round-3.html">Round 3 notes</a></li>
          <li><a href="../review/round-2.html">Round 2 comparison canvas (concepts 4–6)</a></li>
          <li><a href="../notes/round-2.html">Round 2 notes</a></li>
          <li><a href="../review/comparison.html">Round 1 comparison canvas (concepts 1–3)</a></li>
          <li><a href="../notes/round-1.html">Round 1 notes</a></li>
          <li><a href="../index.html">Project launcher (all rounds)</a></li>
        </ul>
      </article>
      <article class="rv-card">
        <h3>Concepts 1–9</h3>
        <ul class="rv-links">
          <li><span>Concept 1 · <a href="../concepts/a/identity.html">identity</a> · <a href="../concepts/a/dashboard.html">dashboard</a> · <a href="../concepts/a/mobile.html">mobile</a> · <a href="../concepts/a/cli.html">CLI</a></span></li>
          <li><span>Concept 2 · <a href="../concepts/b/identity.html">identity</a> · <a href="../concepts/b/dashboard.html">dashboard</a> · <a href="../concepts/b/mobile.html">mobile</a> · <a href="../concepts/b/cli.html">CLI</a></span></li>
          <li><span>Concept 3 · <a href="../concepts/c/identity.html">identity</a> · <a href="../concepts/c/dashboard.html">dashboard</a> · <a href="../concepts/c/mobile.html">mobile</a> · <a href="../concepts/c/cli.html">CLI</a></span></li>
          <li><span>Concept 4 · <a href="../concepts/d/identity.html">identity</a> · <a href="../concepts/d/dashboard.html">dashboard</a> · <a href="../concepts/d/mobile.html">mobile</a> · <a href="../concepts/d/cli.html">CLI</a></span></li>
          <li><span>Concept 5 · <a href="../concepts/e/identity.html">identity</a> · <a href="../concepts/e/dashboard.html">dashboard</a> · <a href="../concepts/e/mobile.html">mobile</a> · <a href="../concepts/e/cli.html">CLI</a></span></li>
          <li><span>Concept 6 · <a href="../concepts/f/identity.html">identity</a> · <a href="../concepts/f/dashboard.html">dashboard</a> · <a href="../concepts/f/mobile.html">mobile</a> · <a href="../concepts/f/cli.html">CLI</a></span></li>
          <li><span>Concept 7 · <a href="../concepts/g/identity.html">identity</a> · <a href="../concepts/g/dashboard.html">dashboard</a> · <a href="../concepts/g/mobile.html">mobile</a> · <a href="../concepts/g/cli.html">CLI</a></span></li>
          <li><span>Concept 8 · <a href="../concepts/h/identity.html">identity</a> · <a href="../concepts/h/dashboard.html">dashboard</a> · <a href="../concepts/h/mobile.html">mobile</a> · <a href="../concepts/h/cli.html">CLI</a></span></li>
          <li><span>Concept 9 · <a href="../concepts/i/identity.html">identity</a> · <a href="../concepts/i/dashboard.html">dashboard</a> · <a href="../concepts/i/mobile.html">mobile</a> · <a href="../concepts/i/cli.html">CLI</a></span></li>
        </ul>
      </article>
    </div>
  </section>
</main>
<footer class="rv-foot"><div class="mk-container rv-foot__in"><span>MainsKeeper · complete-design review checkpoint 0.1.12 · not final</span><span class="mk-fict-tag">Fictional demonstration data</span></div></footer>

<script src="app/assets/fixture.js"></script>
<script src="app/assets/store.js"></script>
<script src="app/assets/config-contract.js"></script>
<script src="app/assets/components.js"></script>
<script>
(function () {
  var root = document.documentElement, btn = document.getElementById('rv-theme');
  function sync() { var light = root.getAttribute('data-theme') === 'light'; btn.setAttribute('aria-pressed', String(light)); btn.textContent = light ? 'Dark theme' : 'Light theme'; }
  btn.addEventListener('click', function () {
    var t = root.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
    root.setAttribute('data-theme', t);
    try { localStorage.setItem('mk-theme', t); } catch (e) {}
    try { var u = new URL(location.href); if (u.searchParams.get('theme')) { u.searchParams.set('theme', t); history.replaceState(null, '', u); } } catch (e) {}
    sync();
  });
  sync();

  /* Coverage totals, counted from the table (static text is the same count). */
  try {
    var n = { designed: 0, partial: 0, not: 0 };
    document.querySelectorAll('#rv-cov .rv-status').forEach(function (s) {
      if (s.classList.contains('rv-status--designed')) n.designed++; else if (s.classList.contains('rv-status--partial')) n.partial++; else n.not++;
    });
    document.getElementById('rv-cov-count').textContent = 'Totals: ' + n.designed + ' Designed · ' + n.partial + ' Partial · ' + n.not + ' Not designed.';
  } catch (e) {}

  var MK = window.MK;

  /* Dataset label and Reset demo (shared prototype state). */
  var ds = document.getElementById('rv-dataset'), rb = document.getElementById('rv-reset');
  function showDataset() {
    try { if (MK && MK.store && ds) ds.textContent = 'Dataset: ' + (MK.store.dataset() === 'account' ? 'Your account (first run)' : 'Demo fleet'); } catch (e) {}
  }
  showDataset();
  if (MK && MK.store && MK.store.subscribe) { try { MK.store.subscribe(showDataset); } catch (e) {} }
  rb.addEventListener('click', function () {
    if (MK && MK.store && MK.store.confirmReset) { MK.store.confirmReset({ returnFocus: rb }); return; }
    if (window.confirm('Reset demo? This clears everything created or changed in this prototype.')) {
      try { localStorage.removeItem('mk-proto-v1'); } catch (e) {}
      showDataset();
    }
  });

  /* Real product badges. */
  if (MK && MK.ui && MK.ui.statusBadge) {
    try {
      document.querySelectorAll('#rv-glyphs dt[data-badge]').forEach(function (dt) { dt.innerHTML = MK.ui.statusBadge(dt.getAttribute('data-badge'), { label: dt.textContent }); });
    } catch (e) {}
  }

  /* Scenario counts — always from the demo incident fixture, whatever dataset is active. */
  if (MK && MK.select && MK.select.fleetCounts && MK.data) {
    try {
      var demo = MK.data('incident', 'default');
      var c = MK.select.fleetCounts(demo);
      var set = function (k, v) { var el = document.querySelector('#rv-counts [data-k="' + k + '"]'); if (el && v != null) el.textContent = v; };
      set('collectors', c.collectorsReporting + ' of ' + c.collectorsExpected);
      set('ups', c.upsCommunicating + ' of ' + c.upsTotal);
      set('onBattery', c.upsOnBattery);
      set('open', c.openIncidents);
      set('rec', c.recentRecoveries);
      var sites = MK.select.siteSummaries(demo);
      if (sites && sites.length) {
        document.getElementById('rv-sites').innerHTML = sites.map(function (s) {
          return '<div><dt>' + MK.ui.esc(s.name) + '</dt><dd>' + MK.ui.esc(s.headline) + '</dd></div>';
        }).join('');
      }
    } catch (e) {}
  }

  /* Configuration ownership — rendered from MK.config.contract (static summary stays if unavailable). */
  if (MK && MK.config && MK.config.contract && MK.ui && MK.ui.esc) {
    try {
      var OWN = { dashboard: 'Dashboard', 'local-secret': 'Local secret', local: 'Local', capability: 'Capability' };
      var e = MK.ui.esc;
      var rows = MK.config.contract.map(function (r) {
        var owner = r.application === 'server' ? 'Dashboard (server)' : (OWN[r.owner] || r.owner);
        var app = r.applicationText || '';
        if (r.overrideAllowed && r.overrideHow) app += ' · Local override: ' + r.overrideHow;
        return '<tr><td>' + e(r.label) + '</td><td>' + e(owner) + '</td><td>' + e(r.editLocation || '') + '</td><td>' + e(r.effectiveSource || '') + '</td><td>' + e(app) + '</td></tr>';
      });
      if (rows.length) document.getElementById('rv-own-body').innerHTML = rows.join('');
    } catch (e2) {}
  }
})();
</script>
</body>
</html>
