# manifest-diff · assets.gf.cx

A shared, single-source **before/after delta UI** — a one-sentence *verdict*
callout that decodes a jump ("+1.1 GB, same object count — existing files
changed size"), paired with a signed-Δ comparison table that itemizes the change
with per-row new/gone tags.

Extracted from the inline styles of `gfcx_r2_manifest.py` — the `_diff_section`
and `_breakdown_table` renderers behind the status.gf.cx
`/r2/<bucket>/manifest/` pages — so every surface draws the *identical* delta UI
from one source of truth (the favicon-control-plane / op-panel single-source
model applied to a diff). First consumer: the R2 file-type manifest pages, where
the verdict answers "what actually changed when the storage sparkline jumped?"

## Use

```html
<link rel="stylesheet" href="https://assets.gf.cx/manifest-diff/manifest-diff.css">

<div class="manifest-diff">
  <div class="verdict up">
    <strong>+1.1 GB</strong> between Aug 12 and Aug 19, same object count —
    <em>existing files changed size</em>. Biggest mover:
    <span class="mono">.cr2</span> +820 MB (+40 files).
    <div class="sub">before: 493,318 objects / 374.9 GB → after: 493,358 objects / 376.0 GB</div>
  </div>

  <table>
    <thead><tr>
      <th>File type</th><th class="num">Δ files</th><th class="num">Δ bytes</th>
      <th class="num">Files now</th><th class="num">Size now</th>
    </tr></thead>
    <tbody>
      <tr><td class="ext">.cr2</td>
          <td class="num pos">+40</td><td class="num pos">+820 MB</td>
          <td class="num">1,091</td><td class="num">1.1 GB</td></tr>
      <tr><td class="ext">.avif<span class="tag new">new</span></td>
          <td class="num pos">+120</td><td class="num pos">+14 MB</td>
          <td class="num">120</td><td class="num">14 MB</td></tr>
      <tr><td class="ext">.bmp<span class="tag gone">gone</span></td>
          <td class="num neg">−18</td><td class="num neg">−44 MB</td>
          <td class="num">0</td><td class="num">0 B</td></tr>
    </tbody>
  </table>
  <div class="cap">3 changed file types · red = grew, green = shrank</div>
</div>
```

CSS-only — no JS. The wrapping `.manifest-diff` is **required**: every rule is
namespaced under it so the module never touches a host page's own bare
`<table>` / `.verdict` / `.tag` rules. The value strings (signed bytes, `±0`,
new/gone) are pre-baked by the emitting renderer; the CSS only paints them.

## Classes

| class                  | on                     | purpose                                                    |
|------------------------|------------------------|------------------------------------------------------------|
| `.verdict`             | callout block          | the one-sentence headline that decodes the delta           |
| `.verdict.up`          | + `.verdict`           | growth — red left-edge border                              |
| `.verdict.down`        | + `.verdict`           | shrink — green left-edge border                            |
| `.sub`                 | inside `.verdict`      | the muted before→after totals line                         |
| `.pos`                 | `td.num`               | positive delta — red (it grew)                             |
| `.neg`                 | `td.num`               | negative delta — green (it shrank)                         |
| `.zero`                | `td.num` / `td.ext`    | no change / folded rows — muted                            |
| `.tag.new`            | inside `td.ext`        | a file type that appeared (red pill)                       |
| `.tag.gone`           | inside `td.ext`        | a file type that vanished (green pill)                     |
| `.cap`                 | after a table          | caption line (type count / sort / capture day)             |
| `.section-label`       | eyebrow before a block | optional uppercase section label (matches the source page) |
| `.empty`               | standalone block       | honest "only one manifest — nothing to diff yet" state     |
| `.num` / `.ext`        | `th` / `td`            | right-aligned mono numeric column / mono file-type column  |
| `.mono`                | any inline span        | mono run inside prose (e.g. the driver ext in the verdict) |

## Custom properties

Every colour reads the portfolio design tokens (`--ink`, `--ink-soft`,
`--accent`, `--line`, `--line-strong`, `--red`, `--green`, `--mono`, `--serif`)
with standalone fallbacks, so the module renders correctly with no host tokens
loaded. Two surface colours can be tuned per instance:

| property                   | purpose                      | default   |
|----------------------------|------------------------------|-----------|
| `--md-diff-surface`        | card / table background      | `#fff`    |
| `--md-diff-surface-alt`    | header + row-hover background | `#faf7f1` |

## Honesty

- **Positive = red, negative = green** — a store *growing* is the thing that
  needs explaining, so growth is the alarm colour; a shrink reads calm.
- **`.empty`** carries the n=1 case (one manifest, nothing to diff) so a linked
  page is never broken before the second capture lands.

## Keep in step

The class set is a verbatim extraction of the inline `<style>` in
`gfcx_r2_manifest.py` (`_diff_section` / `_breakdown_table`). If you change the
markup one emits, change the other. The generator's pages still carry their own
inline copy for now (no deploy-order dependency on this sheet) — adopting this
shared stylesheet there is a deliberate follow-up.

## In use / example

- https://status.gf.cx/r2/dan-immich/manifest/ — 493,358 objects, 18 file types
  (photos, RAW `.cr2`/`.dng`, `.avif`, video) — the rich case.
- https://status.gf.cx/r2/immich-db/manifest/ — 11 `.gz` files, one type — the
  minimal case.

v1.0.0 · 2026-08-19
