Files
familienarchiv/docs/specs/person-dashboard-spec.html
Marcel 94e976bae3
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m15s
CI / OCR Service Tests (push) Successful in 49s
CI / Backend Unit Tests (push) Failing after 3m1s
docs(specs): rework person dashboard spec around data reality
The archive has ~4 persons over 100 letters and ~90% with five or
fewer — the original spec's 851-letter default fit no one.

Redesign introduces three tiers gated on letterCount (Compact ≤ 5,
Standard 6–49, Rich ≥ 50) sharing one dashboard block: navy header +
4-cell stats strip at every non-Empty tier, with Standard appending
direction bar + top correspondents and Rich further appending
histogram + top locations + tag cloud. Backend skips expensive
aggregations for non-Rich persons; histogram and tag cloud ship
lazy-loaded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 20:44:42 +02:00

1146 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Person Detail — Korrespondenz-Überblick · Tiered Edition · Familienarchiv</title>
<style>
/* ═══════════════════════════════════════════════════════════
PERSON DASHBOARD — /persons/[id] · Tiered Edition
Three tiers mapped to the archive's real data distribution:
· Compact — ≤ 5 letters · ~90% of persons (baseline)
· Standard — 649 letters · ~6% of persons
· Rich — ≥ 50 letters · <1% (4 real cases)
The Rich tier is the former 6-block dashboard. Compact replaces
it as the default because the archive is 90% short-correspondence
persons — charts for 3 letters are noise, not signal.
By Leonie Voss (UX/Design). 2026-04-23 · REV 2.
═══════════════════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5;-webkit-font-smoothing:antialiased}
.doc{max-width:1400px;margin:0 auto;padding:48px 28px}
/* ── Masthead ──────────────────────────────────────────── */
.mast{background:#012851;border-radius:10px;padding:32px 40px;margin-bottom:40px;color:#fff}
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:14px}
.mast h1{font-size:22px;font-weight:900;letter-spacing:-.4px;margin-bottom:6px}
.mast p{font-size:12px;color:rgba(255,255,255,.6);max-width:820px;line-height:1.7}
.mast p code{background:rgba(255,255,255,.1);padding:1px 5px;border-radius:2px;font-family:monospace}
.mast p b{color:#fff}
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;flex-shrink:0;margin-top:4px;background:#a1dcd8;color:#012851}
.decisions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:18px;border-top:1px solid rgba(255,255,255,.1);padding-top:14px}
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
.dec-label{font-size:7.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.4);margin-bottom:5px}
.dec-value{font-size:10px;font-weight:700;color:#fff;line-height:1.5}
.dec-value s{color:rgba(255,255,255,.3);font-weight:400}
.spec-disclaimer{background:#FFFBEB;border:1px solid #FCD34D;border-radius:6px;padding:14px 18px;font-size:11.5px;color:#78350F;line-height:1.6;margin-bottom:32px}
.spec-disclaimer strong{color:#92400E}
.spec-disclaimer code{background:#FEF3C7;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
.rev-note{background:#FEF2F2;border:1px solid #FCA5A5;border-radius:6px;padding:14px 18px;font-size:11.5px;color:#7F1D1D;line-height:1.6;margin-bottom:32px}
.rev-note strong{color:#991B1B}
.rev-note code{background:#FEE2E2;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
/* ── TOC ──────────────────────────────────────────── */
.toc{background:#fff;border:1px solid #DDD8CE;border-radius:8px;padding:18px 22px;margin-bottom:40px}
.toc-t{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#666;margin-bottom:10px}
.toc ol{list-style:none;display:grid;grid-template-columns:repeat(2,1fr);gap:6px 24px}
.toc li{font-size:12px;color:#012851;display:flex;align-items:baseline;gap:8px}
.toc li b{background:#012851;color:#fff;padding:1px 6px;border-radius:3px;font-size:9px}
.toc li span{color:#888;font-size:10.5px;margin-left:auto}
/* ── Sections ──────────────────────────────────────── */
.sec{margin-bottom:56px}
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:48px}
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#666;margin-bottom:20px;display:flex;align-items:center;gap:10px}
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
.sec-num{background:#012851;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
.sec-intro{font-size:12.5px;color:#555;line-height:1.65;max-width:820px;margin-bottom:24px}
.sec-intro b{color:#012851}
.sec-intro code{background:#EAE7DC;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:11px;color:#012851}
/* Tier badges */
.tier-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:12px;font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;font-family:'Helvetica Neue',Arial,sans-serif;vertical-align:middle}
.tier-badge.empty{background:#F5F4EF;color:#888;border:1px solid #E4E2D7}
.tier-badge.compact{background:#E4E2D7;color:#555;border:1px solid #D4D1C3}
.tier-badge.standard{background:#a1dcd8;color:#012851}
.tier-badge.rich{background:#012851;color:#fff}
/* Data reality table */
.reality-tbl{width:100%;border-collapse:collapse;background:#fff;border:1px solid #DDD8CE;border-radius:8px;overflow:hidden;font-size:12px;margin-bottom:18px}
.reality-tbl th{background:#F5F4EF;text-align:left;padding:11px 16px;font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#666;border-bottom:1px solid #DDD8CE}
.reality-tbl td{padding:14px 16px;border-bottom:1px solid #F1ECE0;color:#333;vertical-align:top;line-height:1.6}
.reality-tbl tr:last-child td{border-bottom:0}
.reality-tbl td code{background:#F1ECE0;padding:1px 6px;border-radius:3px;font-family:monospace;font-size:11px;color:#012851}
.reality-tbl td.share{font-family:Georgia,serif;font-size:18px;font-weight:900;color:#012851;letter-spacing:-.4px;width:90px}
.reality-tbl td.tier-col{width:120px}
.reality-tbl td.example{color:#666;font-style:italic;font-size:11px}
/* ── Wireframe chrome ────────────────────────────── */
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
.wf-bar{height:22px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 8px;gap:4px}
.dot{width:6px;height:6px;border-radius:50%;background:#C8C4BE;display:inline-block}
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
.urlbar{flex:1;height:10px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
.urlbar span{font-size:7px;color:#888;font-family:monospace}
.wf.dark{background:#010e1e;border-color:#0d3358}
.wf.dark .wf-bar{background:#0d2240;border-bottom-color:#081a30}
.wf.dark .urlbar{background:#012851}
.wf.dark .urlbar span{color:#7a8a9a}
.wf-m{width:176px;border-radius:14px}
.wf-m .wf-bar{display:none}
.wf-m-status{height:13px;background:#012851;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border-top-left-radius:12px;border-top-right-radius:12px}
.wf-m-status span{font-size:6px;color:#fff;font-weight:700}
.wf-m-status .dots{display:flex;gap:2px}
.wf-m-status .dots i{width:4px;height:4px;border-radius:50%;background:rgba(255,255,255,.6)}
.wf-t{width:422px}
.wf-d{width:780px}
/* ── State block ──────────────────────────────────── */
.state-block{background:#fff;border:1px solid #DDD8CE;border-radius:10px;padding:22px;margin-bottom:20px}
.state-hdr{display:flex;align-items:baseline;gap:12px;margin-bottom:4px;flex-wrap:wrap}
.state-num{background:#012851;color:#fff;font-size:9px;font-weight:900;padding:3px 7px;border-radius:10px;flex-shrink:0}
.state-title{font-size:14px;font-weight:800;color:#012851}
.state-desc{font-size:11.5px;color:#555;line-height:1.6;margin-bottom:18px;max-width:820px}
.state-desc code{background:#EAE7DC;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px;color:#012851}
.state-vps{display:grid;gap:20px;grid-template-columns:min-content min-content 1fr;align-items:flex-start}
.state-vp-col{display:flex;flex-direction:column;gap:6px;align-items:center}
.vp-tag{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#fff;background:#012851;padding:3px 8px;border-radius:3px}
.vp-dim{font-size:8px;color:#888;font-family:monospace;margin-top:-2px}
/* ── Person page content (scaled ~55%) ─────────── */
.pp{background:#ECEAE4;font-family:'Helvetica Neue',Arial,sans-serif;color:#012851}
.pp.dark{background:#010e1e;color:#f0efe9}
.pp-gh{height:18px;background:#012851;display:flex;align-items:center;padding:0 10px;gap:8px;border-bottom:1px solid #a1dcd8}
.pp-gh-logo{font-size:6px;font-weight:900;color:#fff;letter-spacing:.12em;text-transform:uppercase}
.pp-gh-nav{display:flex;gap:8px;margin-left:auto}
.pp-gh-nav span{font-size:5.5px;font-weight:600;color:rgba(255,255,255,.55)}
.pp-gh-nav span.on{color:#fff;border-bottom:1px solid #a1dcd8;padding-bottom:1px}
.pp-wrap{padding:8px 8px 12px;max-width:100%}
.wf-t .pp-wrap{padding:14px 18px 18px}
.wf-d .pp-wrap{padding:18px 28px 22px}
.pp-back{font-size:5px;color:#888;font-weight:700;text-transform:uppercase;margin-bottom:6px}
.wf-t .pp-back{font-size:8px;margin-bottom:10px}
.wf-d .pp-back{font-size:10px;margin-bottom:14px}
/* Layout split */
.pp-grid{display:grid;grid-template-columns:35% 1fr;gap:10px}
.wf-m .pp-grid{grid-template-columns:1fr;gap:8px}
.wf-t .pp-grid{gap:14px}
.wf-d .pp-grid{gap:22px}
/* Left: person card (unchanged) */
.pp-card{background:#fff;border:1px solid #e4e2d7;border-radius:2px;padding:10px 8px;display:flex;flex-direction:column;align-items:center;gap:5px}
.pp.dark .pp-card{background:#011a30;border-color:#0d3358}
.wf-t .pp-card{padding:16px 14px;gap:9px}
.wf-d .pp-card{padding:22px 18px;gap:12px}
.pp-av{width:28px;height:28px;border-radius:50%;background:#a1dcd8;color:#012851;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;font-family:Georgia,serif}
.pp.dark .pp-av{background:rgba(166,218,216,.15);color:#a1dcd8}
.wf-t .pp-av{width:50px;height:50px;font-size:16px}
.wf-d .pp-av{width:72px;height:72px;font-size:22px}
.pp-name{font-family:Georgia,serif;font-size:7.5px;font-weight:700;color:#012851;text-align:center;line-height:1.3}
.pp.dark .pp-name{color:#f0efe9}
.wf-t .pp-name{font-size:13px}
.wf-d .pp-name{font-size:17px}
.pp-dates{font-size:5.5px;color:#888;font-weight:700}
.pp.dark .pp-dates{color:#6e7a8a}
.wf-t .pp-dates{font-size:9px}
.wf-d .pp-dates{font-size:11px}
.pp-actions{display:flex;gap:3px;width:100%;margin-top:4px}
.wf-t .pp-actions{gap:5px;margin-top:8px}
.wf-d .pp-actions{gap:6px;margin-top:10px}
.pp-btn{flex:1;height:12px;background:#f7f5f2;border:1px solid #e4e2d7;border-radius:2px;font-size:4.5px;font-weight:800;color:#444;text-transform:uppercase;display:flex;align-items:center;justify-content:center;gap:2px}
.pp.dark .pp-btn{background:#0d2240;border-color:#0d3358;color:#d0d6de}
.pp-btn.primary{background:#012851;color:#fff;border-color:#012851}
.pp.dark .pp-btn.primary{background:#a1dcd8;color:#012851;border-color:#a1dcd8}
.wf-t .pp-btn{height:22px;font-size:7.5px}
.wf-d .pp-btn{height:28px;font-size:9px}
/* ── Shared dashboard chrome (all non-Empty tiers) ─── */
.pp-dash{background:#fff;border:1px solid #e4e2d7;border-radius:2px;overflow:hidden}
.pp.dark .pp-dash{background:#011a30;border-color:#0d3358}
.pp-dash-hdr{background:#012851;color:#fff;padding:4px 6px;display:flex;justify-content:space-between;align-items:center;gap:4px}
.pp.dark .pp-dash-hdr{background:#01223f}
.wf-t .pp-dash-hdr{padding:9px 14px}
.wf-d .pp-dash-hdr{padding:12px 18px}
.pp-dash-hdr h2{font-family:Georgia,serif;font-size:6px;font-weight:700}
.wf-t .pp-dash-hdr h2{font-size:11px}
.wf-d .pp-dash-hdr h2{font-size:14px}
.pp-open-conv{background:#a1dcd8;color:#012851;font-size:4.5px;font-weight:800;padding:1px 4px;border-radius:2px;text-transform:uppercase;letter-spacing:.3px}
.wf-t .pp-open-conv{font-size:7.5px;padding:4px 9px}
.wf-d .pp-open-conv{font-size:9.5px;padding:5px 12px}
/* Stats strip (Standard + Rich) */
.pp-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.5px;background:#e4e2d7;border-bottom:1px solid #e4e2d7}
.pp.dark .pp-stats{background:#0d3358;border-bottom-color:#0d3358}
.pp-stats div{background:#fafaf5;padding:3px 2px;text-align:center}
.pp.dark .pp-stats div{background:#011526}
.pp-stats .v{font-family:Georgia,serif;font-size:7.5px;font-weight:900;color:#012851;letter-spacing:-.3px}
.pp.dark .pp-stats .v{color:#f0efe9}
.wf-t .pp-stats div{padding:8px}
.wf-d .pp-stats div{padding:12px 6px}
.wf-t .pp-stats .v{font-size:16px}
.wf-d .pp-stats .v{font-size:22px}
.pp-stats .v.out{color:#012851}
.pp-stats .v.in{color:#2F9E95}
.pp.dark .pp-stats .v.out{color:#a1dcd8}
.pp.dark .pp-stats .v.in{color:#00c7b1}
.pp-stats .k{font-size:4px;color:#888;font-weight:800;text-transform:uppercase;letter-spacing:.4px}
.pp.dark .pp-stats .k{color:#6e7a8a}
.wf-t .pp-stats .k{font-size:8px}
.wf-d .pp-stats .k{font-size:10px}
/* Dashboard section */
.pp-dsec{padding:4px 6px;border-top:1px solid #f1ede3}
.pp.dark .pp-dsec{border-top-color:#092843}
.pp-dsec:first-of-type{border-top:0}
.wf-t .pp-dsec{padding:12px 14px}
.wf-d .pp-dsec{padding:16px 20px}
.pp-dsec h3{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline}
.pp.dark .pp-dsec h3{color:#6e7a8a}
.wf-t .pp-dsec h3{font-size:8px;margin-bottom:8px}
.wf-d .pp-dsec h3{font-size:10px;margin-bottom:10px}
.pp-dsec h3 .note{font-size:5px;color:#555;font-weight:600;text-transform:none;letter-spacing:0}
.pp.dark .pp-dsec h3 .note{color:#9ca3af}
.wf-t .pp-dsec h3 .note{font-size:8.5px}
.wf-d .pp-dsec h3 .note{font-size:10.5px}
.pp-dsec h3 .note b{color:#012851}
.pp.dark .pp-dsec h3 .note b{color:#f0efe9}
/* Direction split */
.pp-dsplit{display:flex;justify-content:space-between;font-size:5px;font-weight:700;margin-bottom:3px}
.wf-t .pp-dsplit{font-size:9px;margin-bottom:5px}
.wf-d .pp-dsplit{font-size:11px;margin-bottom:7px}
.pp-dsplit .out{color:#012851}
.pp-dsplit .in{color:#2F9E95}
.pp.dark .pp-dsplit .out{color:#a1dcd8}
.pp.dark .pp-dsplit .in{color:#00c7b1}
.pp-dbar{height:3px;display:flex;border-radius:2px;overflow:hidden;background:#e4e2d7}
.pp.dark .pp-dbar{background:#0d3358}
.wf-t .pp-dbar{height:6px}
.wf-d .pp-dbar{height:8px}
.pp-dbar .out{background:#012851}
.pp-dbar .in{background:#2F9E95}
.pp.dark .pp-dbar .out{background:#a1dcd8}
.pp.dark .pp-dbar .in{background:#00c7b1}
/* Top list */
.pp-toplist{display:flex;flex-direction:column;gap:2px}
.wf-t .pp-toplist{gap:4px}
.wf-d .pp-toplist{gap:6px}
.pp-ti{display:flex;align-items:center;gap:3px;font-size:5px;padding:1px 2px;border-radius:2px;cursor:pointer}
.pp-ti:hover{background:#f7f5f2}
.pp.dark .pp-ti:hover{background:rgba(255,255,255,.04)}
.wf-t .pp-ti{font-size:9px;gap:6px;padding:2px 4px}
.wf-d .pp-ti{font-size:11px;gap:9px;padding:3px 6px}
.pp-ti .nm{flex:1;color:#012851;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pp.dark .pp-ti .nm{color:#f0efe9}
.pp-ti .bw{width:34px;height:2px;background:#e4e2d7;border-radius:1px;overflow:hidden;flex-shrink:0}
.pp.dark .pp-ti .bw{background:#0d3358}
.wf-t .pp-ti .bw{width:60px;height:4px;border-radius:2px}
.wf-d .pp-ti .bw{width:90px;height:6px;border-radius:3px}
.pp-ti .bw span{display:block;height:100%;background:#012851}
.pp.dark .pp-ti .bw span{background:#a1dcd8}
.pp-ti .val{width:14px;text-align:right;font-size:4.5px;color:#888;font-weight:700;font-variant-numeric:tabular-nums}
.pp.dark .pp-ti .val{color:#6e7a8a}
.wf-t .pp-ti .val{width:22px;font-size:8px}
.wf-d .pp-ti .val{width:30px;font-size:10px}
/* Histogram (Rich only) */
.pp-hist{display:flex;align-items:flex-end;gap:.5px;height:20px;padding:2px 0 0}
.pp-hist .bar{flex:1;background:#a1dcd8;opacity:.65;border-radius:.5px .5px 0 0;cursor:pointer}
.pp.dark .pp-hist .bar{background:#00c7b1;opacity:.6}
.pp-hist .bar.peak{background:#012851;opacity:.9}
.pp.dark .pp-hist .bar.peak{background:#a1dcd8;opacity:.9}
.wf-t .pp-hist{height:50px;gap:1px}
.wf-d .pp-hist{height:72px;gap:1px}
.pp-hist-labels{display:flex;justify-content:space-between;font-size:4px;color:#888;margin-top:2px;font-weight:700}
.wf-t .pp-hist-labels{font-size:8px;margin-top:5px}
.wf-d .pp-hist-labels{font-size:10px;margin-top:6px}
/* Two-col arrangement (Rich only) */
.pp-twocol{display:grid;grid-template-columns:1fr 1fr;gap:0}
.pp-twocol > div{border-left:1px solid #f1ede3;padding-left:4px}
.pp-twocol > div:first-child{border-left:0;padding-left:0}
.pp.dark .pp-twocol > div{border-left-color:#092843}
.wf-m .pp-twocol{grid-template-columns:1fr}
.wf-m .pp-twocol > div{border-left:0;border-top:1px solid #f1ede3;padding-left:0;padding-top:4px;margin-top:4px}
/* Tag cloud (Rich only) */
.pp-cloud{display:flex;flex-wrap:wrap;gap:1px}
.wf-t .pp-cloud{gap:4px}
.wf-d .pp-cloud{gap:5px}
.pp-cloud .tag{cursor:pointer;padding:.5px 2px;background:#a1dcd8;color:#012851;font-weight:700;border-radius:3px;font-size:4.5px}
.pp.dark .pp-cloud .tag{background:rgba(0,199,177,.2);color:#00c7b1}
.wf-t .pp-cloud .tag{font-size:8px;padding:2px 7px;border-radius:10px}
.wf-d .pp-cloud .tag{font-size:10px;padding:2px 9px;border-radius:12px}
.pp-cloud .tag.xl{font-size:6px}
.wf-t .pp-cloud .tag.xl{font-size:12px;padding:3px 10px}
.wf-d .pp-cloud .tag.xl{font-size:14px;padding:3px 11px}
.pp-cloud .tag.l{font-size:5.5px}
.wf-t .pp-cloud .tag.l{font-size:10px;padding:2px 8px}
.wf-d .pp-cloud .tag.l{font-size:12px;padding:3px 10px}
.pp-cloud .tag.m{font-size:5px}
.wf-t .pp-cloud .tag.m{font-size:9px}
.wf-d .pp-cloud .tag.m{font-size:11px}
.pp-cloud .tag.muted{background:#EEE8DC;color:#666;font-weight:600}
.pp.dark .pp-cloud .tag.muted{background:rgba(255,255,255,.06);color:#9ca3af}
/* Fake document list under the dashboard (existing PersonDocumentList) */
.pp-doclist{background:#fff;border:1px solid #e4e2d7;border-radius:2px;margin-top:4px;padding:4px 6px}
.pp.dark .pp-doclist{background:#011a30;border-color:#0d3358}
.wf-t .pp-doclist{margin-top:10px;padding:10px 14px}
.wf-d .pp-doclist{margin-top:14px;padding:14px 20px}
.pp-doclist-h{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:3px;display:flex;justify-content:space-between}
.pp.dark .pp-doclist-h{color:#6e7a8a}
.wf-t .pp-doclist-h{font-size:8px;margin-bottom:6px}
.wf-d .pp-doclist-h{font-size:10px;margin-bottom:8px}
.pp-docrow{display:flex;align-items:center;gap:3px;font-size:5px;padding:2px 0;border-top:1px solid #f5f3ec}
.pp-docrow:first-child{border-top:0}
.pp.dark .pp-docrow{border-top-color:#0a1f33}
.wf-t .pp-docrow{font-size:9px;padding:4px 0;gap:6px}
.wf-d .pp-docrow{font-size:10.5px;padding:6px 0;gap:9px}
.pp-docrow .d{color:#888;font-family:monospace;font-size:4.5px;width:26px;flex-shrink:0;font-weight:700}
.wf-t .pp-docrow .d{font-size:8px;width:52px}
.wf-d .pp-docrow .d{font-size:10px;width:64px}
.pp-docrow .t{flex:1;color:#012851;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pp.dark .pp-docrow .t{color:#f0efe9}
/* Empty dashboard */
.pp-empty{padding:20px 8px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:3px}
.wf-t .pp-empty{padding:40px 20px;gap:5px}
.wf-d .pp-empty{padding:60px 40px;gap:8px}
.pp-empty-t{font-family:Georgia,serif;font-size:7px;font-weight:700;color:#012851}
.pp.dark .pp-empty-t{color:#f0efe9}
.wf-t .pp-empty-t{font-size:13px}
.wf-d .pp-empty-t{font-size:16px}
.pp-empty-b{font-size:5px;color:#555;line-height:1.55;max-width:130px}
.pp.dark .pp-empty-b{color:#9ca3af}
.wf-t .pp-empty-b{font-size:8.5px;max-width:260px}
.wf-d .pp-empty-b{font-size:11px;max-width:380px}
/* Skeleton */
.sk{background:linear-gradient(90deg,#f5f4ef,#eceae4,#f5f4ef);border-radius:1px;animation:shimmer 1.4s infinite;background-size:200px 100%}
.pp.dark .sk{background:linear-gradient(90deg,#011a30,#011526,#011a30);background-size:200px 100%}
@keyframes shimmer{0%{background-position:-200px 0}100%{background-position:200px 0}}
/* Callouts */
.ann{background:#EFF6FF;border:1px solid #BFDBFE;border-radius:6px;padding:14px 16px;margin-top:16px;font-size:11.5px;color:#1E3A5F;line-height:1.6}
.ann strong{color:#0D2240;display:block;margin-bottom:4px}
.ann ul{margin-top:4px;padding-left:18px}
.ann ul li{margin-top:3px}
.ann code{background:#DBEAFE;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
.callout-grid{display:flex;flex-direction:column;gap:10px;padding-top:18px}
.callout-grid .cg{background:#fff;border:1px solid #DDD8CE;border-radius:6px;padding:12px 14px}
.callout-grid .cg .cg-t{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#012851;margin-bottom:4px}
.callout-grid .cg .cg-b{font-size:10.5px;color:#444;line-height:1.55}
/* Impl-ref */
.impl-ref{background:#0d1117;border-radius:7px;margin-top:16px;overflow:hidden;border:1px solid #30363d}
.impl-ref-hdr{background:#161b22;padding:8px 16px;font-size:9.5px;font-weight:800;color:#f0883e;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:8px;letter-spacing:.4px;text-transform:uppercase}
.impl-ref-hdr::before{content:'⚙';font-size:12px}
.impl-ref-hdr span{color:rgba(240,136,62,.55);font-weight:400;margin-left:auto;font-size:9px;text-transform:none;letter-spacing:0}
.impl-ref table{width:100%;border-collapse:collapse;font-size:10px}
.impl-ref th{text-align:left;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#8b949e;padding:7px 14px;border-bottom:1px solid #21262d}
.impl-ref td{padding:6px 14px;border-bottom:1px solid #161b22;vertical-align:top;line-height:1.5;color:#c9d1d9}
.impl-ref tr:last-child td{border-bottom:none}
.impl-ref td:first-child{color:#79c0ff;font-weight:700;white-space:nowrap;width:220px}
.impl-ref td code{font-family:'SFMono-Regular',Consolas,monospace;font-size:9.5px;background:#161b22;color:#a5d6ff;padding:1px 5px;border-radius:3px}
.impl-ref .ir-px{color:#7ee787;font-family:monospace;font-size:9.5px}
</style>
</head>
<body>
<div class="doc">
<!-- ═══════════════════════════════════════════════════════════
MASTHEAD
═══════════════════════════════════════════════════════════ -->
<header class="mast">
<div class="mast-top">
<div>
<h1>Person Detail — Korrespondenz-Überblick · Tiered Edition</h1>
<p>Redesign of <code>/persons/[id]</code>. The first spec assumed the Walter de&#8239;Gruyter case (851 letters · 42 years · 87 correspondents) was the default layout — it is not. In this archive, <b>~90&#8239;% of persons have ≤&#8239;5 letters</b>, and only <b>4 persons exceed 100</b>. This edition inverts the polarity: one shared dashboard block (navy header + 4-cell stats strip) is the baseline at every tier. <b>Compact</b> ends there and covers ~90&#8239;% of pages; <b>Standard</b> and <b>Rich</b> append progressive sections below the stats strip only when data density earns them.</p>
</div>
<div class="mast-badge">REV&#8239;2 · FINAL</div>
</div>
<div class="decisions">
<div class="dec"><div class="dec-label">Tier thresholds</div><div class="dec-value">Compact&#8239;&#8239;5 · Standard&#8239;649 · Rich&#8239;&#8239;50</div></div>
<div class="dec"><div class="dec-label">Visual system</div><div class="dec-value">One dashboard block · shared navy header + stats strip at every tier</div></div>
<div class="dec"><div class="dec-label">Progressive sections</div><div class="dec-value">Standard adds direction + top list · Rich adds histogram + locations + cloud</div></div>
<div class="dec"><div class="dec-label">Rich-only blocks</div><div class="dec-value">Activity histogram · tag cloud <s>(eager)</s> → lazy-loaded</div></div>
</div>
</header>
<div class="rev-note">
<strong>What changed from REV&#8239;1.</strong> REV&#8239;1 shipped a six-block dashboard on every person page. REV&#8239;1's "Sparse" state (<code>letterCount &lt; 10</code>) was treated as the edge case; in reality it is the archive's baseline. REV&#8239;2 makes Compact the default, drops the histogram and tag cloud from the Standard tier, and gates the original six-block treatment behind <code>letterCount ≥ 50</code>. Backend aggregations for the heavy blocks are skipped entirely at Compact and Standard tiers.
</div>
<div class="spec-disclaimer">
<strong>Reading this spec.</strong> Mockups in Sections&#8239;0305 are scaled to ~55&#8239;% of real pixel values so that multiple viewports fit on one page. <strong>Never copy pixel sizes from the mockups.</strong> Use the <code>impl-ref</code> tables for exact Tailwind classes and real pixel values.
</div>
<!-- TOC -->
<div class="toc">
<div class="toc-t">Inhalt</div>
<ol>
<li><b>01</b> Data reality &amp; design response <span>why we rebuilt this</span></li>
<li><b>02</b> Tier decision logic <span>thresholds · what each tier shows</span></li>
<li><b>03</b> Compact tier — the 90&#8239;%&#8239;case <span>3 viewports</span></li>
<li><b>04</b> Standard tier — middle volume <span>3 viewports</span></li>
<li><b>05</b> Rich tier — archival giants <span>3 viewports · 4 persons qualify</span></li>
<li><b>06</b> Empty &amp; loading states <span>new person · in-flight</span></li>
<li><b>07</b> Accessibility contract <span>WCAG AA/AAA</span></li>
<li><b>08</b> Implementation notes <span>tiered API · lazy components · shipping order</span></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════
SECTION 01 — DATA REALITY
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">01</span>Data Reality — Why We Rebuilt This</h2>
<p class="sec-intro">A good dashboard respects the shape of the archive it describes. As of <b>2026-04-23</b> the distribution of letters per person in the Familienarchiv looks like this:</p>
<table class="reality-tbl">
<thead>
<tr><th>Tier</th><th>Criterion</th><th>Share</th><th>Example</th><th>What it needs</th></tr>
</thead>
<tbody>
<tr>
<td class="tier-col"><span class="tier-badge empty">Empty</span></td>
<td><code>letterCount&#8239;==&#8239;0</code></td>
<td class="share"></td>
<td class="example">Newly imported person · no documents yet</td>
<td>Reassurance card. Charts of nothing are noise.</td>
</tr>
<tr>
<td class="tier-col"><span class="tier-badge compact">Compact</span></td>
<td><code>1 ≤ letterCount ≤ 5</code></td>
<td class="share">~&#8239;90&#8239;%</td>
<td class="example">Elsbeth Brandt · 3 letters · 19191922</td>
<td>A single stat line. The sent/received lists below already answer "which letters?".</td>
</tr>
<tr>
<td class="tier-col"><span class="tier-badge standard">Standard</span></td>
<td><code>6 ≤ letterCount ≤ 49</code></td>
<td class="share">~&#8239;6&#8239;%</td>
<td class="example">Herbert Cram · 18 letters · 8 correspondents</td>
<td>Stats strip · direction bar · top&#8209;5 correspondents tile list.</td>
</tr>
<tr>
<td class="tier-col"><span class="tier-badge rich">Rich</span></td>
<td><code>letterCount ≥ 50</code></td>
<td class="share">&lt;&#8239;1&#8239;%</td>
<td class="example">Walter de Gruyter · 851 letters · 87 corresp. · 42 years</td>
<td>Everything in Standard <em>plus</em> activity histogram · top locations · tag cloud.</td>
</tr>
</tbody>
</table>
<div class="ann">
<strong>The cost of REV&#8239;1 and the savings of REV&#8239;2.</strong>
<ul>
<li>REV&#8239;1 planned six eager components on every person page — stats strip, activity histogram, direction split, top correspondents, top locations, tag cloud.</li>
<li>For the 90&#8239;% of persons with ≤&#8239;5 letters, four of those blocks were either hidden or collapsed to placeholders. Build cost went to invisible UI.</li>
<li>Backend aggregations for the heavy blocks (<code>activityByYear</code>, <code>topLocations</code>, <code>topTags</code>) ran for every person page load regardless of relevance.</li>
<li>REV&#8239;2 ships three tiers: Compact is trivial to build and serves 90&#8239;%, Standard serves the middle, Rich is the only tier that earns the original six-block treatment.</li>
</ul>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 02 — TIER DECISION LOGIC
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">02</span>Tier Decision Logic</h2>
<p class="sec-intro">The tier is derived from one number: <code>letterCount = outCount + inCount</code>. The backend uses the same rule to decide which aggregations to compute, so expensive queries (per-year histogram, tag tallies) are skipped for ~95&#8239;% of persons.</p>
<div class="impl-ref">
<div class="impl-ref-hdr">Tier thresholds<span>server + client use the same rule</span></div>
<table>
<thead><tr><th>letterCount</th><th>Tier</th><th>Blocks shown</th><th>Expensive queries?</th></tr></thead>
<tbody>
<tr><td>0</td><td>EMPTY</td><td>Reassurance card only</td><td>No</td></tr>
<tr><td>1 5</td><td>COMPACT</td><td>Shared dashboard chrome: header + CTA + 4-cell stats strip</td><td>No</td></tr>
<tr><td>6 49</td><td>STANDARD</td><td>Stats strip · direction bar · top correspondents (≤&#8239;5)</td><td>Partial: top correspondents only</td></tr>
<tr><td>≥ 50</td><td>RICH</td><td>All of Standard · activity histogram · top locations · tag cloud</td><td>Yes: full set</td></tr>
</tbody>
</table>
</div>
<div class="ann">
<strong>Why 5 and why 50?</strong>
<ul>
<li><b>5</b> is the point below which a histogram has fewer bars than x-axis ticks, a "top list" is just <em>the</em> list, and a tag cloud shows ≤&#8239;3 tags total. Below 5 letters, the sent/received document lists immediately below the stats strip <em>are</em> the per-letter detail view.</li>
<li><b>50</b> is the point at which per-year density begins to average ≥&#8239;1 letter / year across a ~40-year lifespan — the histogram finally reveals shape rather than scattered dots. It is also the threshold at which "top locations" holds more than 23 entries and a tag cloud distinguishes sizes meaningfully.</li>
<li>Between 5 and 50 the data is too thin for histograms but thick enough that raw document lists become hard to scan — that is what Standard's stats strip and top-correspondents tile solve.</li>
</ul>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 03 — COMPACT TIER
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">03</span>Compact Tier &nbsp;<span class="tier-badge compact">Compact · ~90&#8239;%</span></h2>
<p class="sec-intro">The <b>shared dashboard chrome</b> is the anchor of every non-Empty tier — navy header with the title and "↗ Briefwechsel öffnen" CTA, followed by the 4-cell stats strip. At Compact, that's where the dashboard ends. No other sections, no extra widgets. The sent &amp; received <code>PersonDocumentList</code> components below are unchanged and carry the per-letter detail.</p>
<div class="state-block">
<div class="state-hdr"><span class="state-num">01</span><span class="state-title">Elsbeth Brandt · 3 Briefe · 1919 1922</span>&nbsp;<span class="tier-badge compact">Compact</span></div>
<div class="state-desc">Three total letters — two sent, one received, across a 4-year span. The dashboard renders header + stats and stops. The four stat cells (<code>gesamt · ausgehend · eingehend · Jahre</code>) are the same four cells shown at Standard and Rich — exactly the same markup, just without the sections that would render below when the data warrants.</div>
<div class="state-vps">
<!-- 320 -->
<div class="state-vp-col">
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
<div class="wf wf-m">
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 1963</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv"></a></div>
<div class="pp-stats">
<div><div class="v">3</div><div class="k">ges.</div></div>
<div><div class="v out">2</div><div class="k"></div></div>
<div><div class="v in">1</div><div class="k"></div></div>
<div><div class="v">4J</div><div class="k">Jahre</div></div>
</div>
</div>
<div class="pp-doclist">
<div class="pp-doclist-h"><span>Gesendet · 2</span></div>
<div class="pp-docrow"><span class="d">1919</span><span class="t">Brief an W.</span></div>
<div class="pp-docrow"><span class="d">1922</span><span class="t">Brief an H.</span></div>
</div>
<div class="pp-doclist">
<div class="pp-doclist-h"><span>Empfangen · 1</span></div>
<div class="pp-docrow"><span class="d">1920</span><span class="t">Antwort</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 768 -->
<div class="state-vp-col">
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
<div class="wf wf-t">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/elsbeth</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 1963</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div style="display:flex;flex-direction:column;gap:0">
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
<div class="pp-stats">
<div><div class="v">3</div><div class="k">gesamt</div></div>
<div><div class="v out">2</div><div class="k">ausgehend</div></div>
<div><div class="v in">1</div><div class="k">eingehend</div></div>
<div><div class="v">4</div><div class="k">Jahre</div></div>
</div>
</div>
<div class="pp-doclist">
<div class="pp-doclist-h"><span>Gesendet · 2</span><span>Empfangen · 1</span></div>
<div class="pp-docrow"><span class="d">26.03.1919</span><span class="t">An Walter de Gruyter — Genesungswünsche</span></div>
<div class="pp-docrow"><span class="d">14.08.1922</span><span class="t">An Herbert Cram — Einladung</span></div>
<div class="pp-docrow"><span class="d">02.11.1920</span><span class="t">Von Walter de Gruyter — Dank</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 1440 -->
<div class="state-vp-col">
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
<div class="wf wf-d">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/elsbeth-…</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 1963</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div style="display:flex;flex-direction:column;gap:0">
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
<div class="pp-stats">
<div><div class="v">3</div><div class="k">Briefe gesamt</div></div>
<div><div class="v out">2</div><div class="k">ausgehend</div></div>
<div><div class="v in">1</div><div class="k">eingehend</div></div>
<div><div class="v">4</div><div class="k">Jahre</div></div>
</div>
</div>
<div class="pp-doclist">
<div class="pp-doclist-h"><span>Gesendet · 2 Briefe</span></div>
<div class="pp-docrow"><span class="d">26.03.1919</span><span class="t">An Walter de Gruyter — Genesungswünsche nach Lazarettaufenthalt</span></div>
<div class="pp-docrow"><span class="d">14.08.1922</span><span class="t">An Herbert Cram — Einladung zur Sommerreise nach Bad Kissingen</span></div>
</div>
<div class="pp-doclist">
<div class="pp-doclist-h"><span>Empfangen · 1 Brief</span></div>
<div class="pp-docrow"><span class="d">02.11.1920</span><span class="t">Von Walter de Gruyter — Dank für Geburtstagsgrüße</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ann">
<strong>Why not more sections?</strong> At 3 letters, a histogram would be 3 dots on a 4-year axis — decorative, not informative. A top-correspondents list would be 2 rows with no proportional meaning. A tag cloud would show at most 2 tags. The document rows below already surface every atom of per-letter data; adding derived views on top of 3 data points wastes vertical space and introduces noise.
</div>
<div class="ann">
<strong>Design invariant.</strong> The dashboard block is <em>the same component</em> across Compact, Standard, and Rich — same chrome, same stats strip as the always-rendered baseline. Higher tiers append more sections <em>below</em> the stats. This is the system's consistency contract — you never look at two person pages and see two fundamentally different dashboards, only the same dashboard with more or fewer sections.
</div>
</div>
<div class="impl-ref">
<div class="impl-ref-hdr">Shared dashboard chrome<span>renders at every non-Empty tier · the Compact tier renders only this</span></div>
<table>
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Container</td><td><code>overflow-hidden rounded-sm border border-line bg-surface shadow-sm</code></td><td class="ir-px">1&#8239;px border</td><td>Matches the card pattern from CLAUDE.md</td></tr>
<tr><td>Header strip</td><td><code>flex items-center justify-between gap-3 bg-primary text-primary-fg px-5 py-3</code></td><td class="ir-px">12&#8239;px y padding</td><td>Dark navy — the visual anchor that makes this block recognisable at every tier</td></tr>
<tr><td>Title</td><td><code>font-serif text-base font-bold</code> · text: <code>m.person_dashboard_title()</code> → "Korrespondenz-Überblick"</td><td class="ir-px">16&#8239;px / 700</td><td>Merriweather. Abbreviates to "Überblick" at &lt; 640&#8239;px</td></tr>
<tr><td>CTA "↗ Briefwechsel öffnen"</td><td><code>bg-accent text-primary text-xs font-extrabold uppercase tracking-wide px-3 py-1.5 rounded-sm min-h-[44px] min-w-[44px] inline-flex items-center gap-1.5</code></td><td class="ir-px">44&#8239;px min</td><td>WCAG 2.2 touch target. Mobile shows "↗" only; tablet+ shows the full label</td></tr>
<tr><td>Stats strip container</td><td><code>grid grid-cols-2 sm:grid-cols-4 gap-px bg-line</code></td><td class="ir-px">1&#8239;px gap</td><td>Separators are the parent background showing through</td></tr>
<tr><td>Stat cell</td><td><code>bg-muted px-4 py-3.5 text-center</code></td><td class="ir-px">14&#8239;px y padding</td><td>Uses <code>bg-muted</code> (not <code>bg-surface</code>) so the gap-px separators read</td></tr>
<tr><td>Stat number</td><td><code>font-serif text-[22px] font-black leading-none tabular-nums tracking-tight</code></td><td class="ir-px">22&#8239;px / 900</td><td>Merriweather Black · <code>.out</code> = primary, <code>.in</code> = accent-fg</td></tr>
<tr><td>Stat label</td><td><code>mt-1 text-[10px] font-bold uppercase tracking-wide text-ink-3</code></td><td class="ir-px">10&#8239;px</td><td>Responsive abbreviation: "Briefe gesamt" → "gesamt" → "ges." via Paraglide <code>_short</code> keys</td></tr>
<tr><td>Semantic wrapper</td><td><code>&lt;dl&gt;</code> with <code>&lt;dt&gt;/&lt;dd&gt;</code> pairs</td><td class="ir-px"></td><td>Screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4"</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 04 — STANDARD TIER
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">04</span>Standard Tier &nbsp;<span class="tier-badge standard">Standard · 649</span></h2>
<p class="sec-intro">For persons with meaningful volume but not enough density for charts. The dashboard grows a header strip, a 4-cell stats block, a proportional direction bar, and a top-correspondents tile list — all server-computed and cheap. The histogram, locations, and tag cloud stay off. The top-correspondents list replaces the legacy <code>CoCorrespondentsList</code> in this tier.</p>
<div class="state-block">
<div class="state-hdr"><span class="state-num">02</span><span class="state-title">Herbert Cram · 18 Briefe · 1905 1934</span>&nbsp;<span class="tier-badge standard">Standard</span></div>
<div class="state-desc">Eighteen letters across 30 years, 8 distinct correspondents. The stats strip gives exact counts; the direction bar shows the letter balance at a glance; the top-5 list reveals who Herbert wrote to or received from most often. No year-level histogram yet — density is still too thin for a readable chart.</div>
<div class="state-vps">
<!-- 320 -->
<div class="state-vp-col">
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
<div class="wf wf-m">
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 1967</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv"></a></div>
<div class="pp-stats">
<div><div class="v">18</div><div class="k">ges.</div></div>
<div><div class="v out">11</div><div class="k"></div></div>
<div><div class="v in">7</div><div class="k"></div></div>
<div><div class="v">30J</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Richtung</h3>
<div class="pp-dsplit"><span class="out">&#8239;61%</span><span class="in">&#8239;39%</span></div>
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
</div>
<div class="pp-dsec">
<h3>Top Korrespondenten</h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">W. de Gruyter</span><span class="val">6</span></div>
<div class="pp-ti"><span class="nm">E. Brandt</span><span class="val">4</span></div>
<div class="pp-ti"><span class="nm">G. Rofden</span><span class="val">3</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 768 -->
<div class="state-vp-col">
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
<div class="wf wf-t">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/herbert-cram</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 1967</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
<div class="pp-stats">
<div><div class="v">18</div><div class="k">gesamt</div></div>
<div><div class="v out">11</div><div class="k">ausgehend</div></div>
<div><div class="v in">7</div><div class="k">eingehend</div></div>
<div><div class="v">30</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Richtungsverteilung</h3>
<div class="pp-dsplit"><span class="out">→ 11 · 61%</span><span class="in">← 7 · 39%</span></div>
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
</div>
<div class="pp-dsec">
<h3>Top Korrespondenten <span class="note">5 von 8</span></h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">Walter de Gruyter</span><span class="bw"><span style="width:100%"></span></span><span class="val">6</span></div>
<div class="pp-ti"><span class="nm">Elsbeth Brandt</span><span class="bw"><span style="width:67%"></span></span><span class="val">4</span></div>
<div class="pp-ti"><span class="nm">Gertrud Rofden</span><span class="bw"><span style="width:50%"></span></span><span class="val">3</span></div>
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:33%"></span></span><span class="val">2</span></div>
<div class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:17%"></span></span><span class="val">1</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 1440 -->
<div class="state-vp-col">
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
<div class="wf wf-d">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/herbert-cram-…</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 1967</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
<div class="pp-stats">
<div><div class="v">18</div><div class="k">Briefe gesamt</div></div>
<div><div class="v out">11</div><div class="k">ausgehend</div></div>
<div><div class="v in">7</div><div class="k">eingehend</div></div>
<div><div class="v">30</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Richtungsverteilung</h3>
<div class="pp-dsplit"><span class="out">→ 11 ausgehend · 61%</span><span class="in">← 7 eingehend · 39%</span></div>
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
</div>
<div class="pp-dsec">
<h3>Top Korrespondenten <span class="note">5 von 8 · Zeitraum 1905 1934</span></h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">Walter de Gruyter</span><span class="bw"><span style="width:100%"></span></span><span class="val">6</span></div>
<div class="pp-ti"><span class="nm">Elsbeth Brandt</span><span class="bw"><span style="width:67%"></span></span><span class="val">4</span></div>
<div class="pp-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:50%"></span></span><span class="val">3</span></div>
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:33%"></span></span><span class="val">2</span></div>
<div class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:17%"></span></span><span class="val">1</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ann">
<strong>Why no histogram at this tier?</strong> With 18 letters over 30 years, a histogram averages 0.6 letters/year. Most bars would be empty or 1-tall — noise around a few spikes. Users glean nothing they don't already read in the "30 Jahre" stat. Histograms are a shape-reading tool; they need density to reveal shape.
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 05 — RICH TIER
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">05</span>Rich Tier &nbsp;<span class="tier-badge rich">Rich · ≥&#8239;50</span></h2>
<p class="sec-intro">Reserved for the archival giants — currently the four persons with correspondence volumes above 50 letters (Walter de Gruyter, Walter Dieckmann, Herbert Cram's extended record, Ella Dieckmann). Here the full six-block dashboard earns its place: histogram reveals decade-scale activity, top-locations surface travel patterns, tag cloud shows recurring themes. This is the <em>only</em> tier that warrants the heavy lift.</p>
<div class="state-block">
<div class="state-hdr"><span class="state-num">03</span><span class="state-title">Walter de Gruyter · 851 Briefe · 1898 1940 · 87 Korrespondenten</span>&nbsp;<span class="tier-badge rich">Rich</span></div>
<div class="state-desc">The archival giant. All blocks render. The histogram has one bar per year (1898 → 1940 = 43 bars); peak bar highlighted in primary. Top correspondents &amp; top locations sit in a 2-column grid. Tag cloud shows frequency-sized chips, with muted tags for counts &lt; 5. Every tile is a deep link into <code>/briefwechsel</code> with filters pre-applied.</div>
<div class="state-vps">
<!-- 320 -->
<div class="state-vp-col">
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
<div class="wf wf-m">
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv"></a></div>
<div class="pp-stats">
<div><div class="v">851</div><div class="k">ges.</div></div>
<div><div class="v out">612</div><div class="k"></div></div>
<div><div class="v in">239</div><div class="k"></div></div>
<div><div class="v">42J</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Aktivität</h3>
<div class="pp-hist"><div class="bar" style="height:15%"></div><div class="bar" style="height:40%"></div><div class="bar" style="height:60%"></div><div class="bar" style="height:75%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:70%"></div><div class="bar" style="height:40%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:8%"></div></div>
</div>
<div class="pp-dsec">
<h3>Top</h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">W. Dieckmann</span><span class="val">184</span></div>
<div class="pp-ti"><span class="nm">H. Cram</span><span class="val">143</span></div>
<div class="pp-ti"><span class="nm">E. Dieckmann</span><span class="val">88</span></div>
</div>
</div>
<div class="pp-dsec">
<h3>Schlagwörter</h3>
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag l">Familie</span><span class="tag">Geburtstag</span><span class="tag m">Kur</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 768 -->
<div class="state-vp-col">
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
<div class="wf wf-t">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/walter-de-gruyter</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div><div class="pp-actions"><div class="pp-btn">Edit</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
<div class="pp-stats">
<div><div class="v">851</div><div class="k">gesamt</div></div>
<div><div class="v out">612</div><div class="k">ausgehend</div></div>
<div><div class="v in">239</div><div class="k">eingehend</div></div>
<div><div class="v">42</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Aktivität <span class="note"><b>1922 · 78</b></span></h3>
<div class="pp-hist"><div class="bar" style="height:10%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:30%"></div><div class="bar" style="height:42%"></div><div class="bar" style="height:55%"></div><div class="bar" style="height:68%"></div><div class="bar" style="height:82%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:72%"></div><div class="bar" style="height:56%"></div><div class="bar" style="height:42%"></div><div class="bar" style="height:30%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:12%"></div><div class="bar" style="height:6%"></div></div>
<div class="pp-hist-labels"><span>1898</span><span>1922</span><span>1940</span></div>
</div>
<div class="pp-dsec">
<h3>Richtung</h3>
<div class="pp-dsplit"><span class="out">→ 612 · 72%</span><span class="in">← 239 · 28%</span></div>
<div class="pp-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
</div>
<div class="pp-twocol">
<div class="pp-dsec">
<h3>Top Korresp.</h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">W. Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
<div class="pp-ti"><span class="nm">H. Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
<div class="pp-ti"><span class="nm">E. Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
<div class="pp-ti"><span class="nm">E. de Gruyter</span><span class="bw"><span style="width:42%"></span></span><span class="val">77</span></div>
</div>
</div>
<div class="pp-dsec">
<h3>Top Orte</h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
<div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
<div class="pp-ti"><span class="nm">Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
<div class="pp-ti"><span class="nm">Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
</div>
</div>
</div>
<div class="pp-dsec">
<h3>Schlagwörter</h3>
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag xl">Familie</span><span class="tag l">Geburtstag</span><span class="tag l">Weihnachten</span><span class="tag m">Kur</span><span class="tag m">Reise</span><span class="tag">Krieg</span><span class="tag muted">Krankheit</span><span class="tag muted">Tod</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 1440 -->
<div class="state-vp-col">
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
<div class="wf wf-d">
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/walter-de-gruyter-…</span></div></div>
<div class="pp">
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
<div class="pp-wrap">
<div class="pp-back">← Zurück</div>
<div class="pp-grid">
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
<div class="pp-stats">
<div><div class="v">851</div><div class="k">Briefe gesamt</div></div>
<div><div class="v out">612</div><div class="k">ausgehend</div></div>
<div><div class="v in">239</div><div class="k">eingehend</div></div>
<div><div class="v">42</div><div class="k">Jahre</div></div>
</div>
<div class="pp-dsec">
<h3>Aktivität über die Jahre <span class="note">Spitzenjahr <b>1922 · 78 Briefe</b></span></h3>
<div class="pp-hist"><div class="bar" style="height:12%"></div><div class="bar" style="height:18%"></div><div class="bar" style="height:26%"></div><div class="bar" style="height:38%"></div><div class="bar" style="height:44%"></div><div class="bar" style="height:52%"></div><div class="bar" style="height:60%"></div><div class="bar" style="height:68%"></div><div class="bar" style="height:80%"></div><div class="bar" style="height:88%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:72%"></div><div class="bar" style="height:58%"></div><div class="bar" style="height:48%"></div><div class="bar" style="height:38%"></div><div class="bar" style="height:28%"></div><div class="bar" style="height:22%"></div><div class="bar" style="height:18%"></div><div class="bar" style="height:14%"></div><div class="bar" style="height:10%"></div><div class="bar" style="height:6%"></div><div class="bar" style="height:4%"></div><div class="bar" style="height:2%"></div></div>
<div class="pp-hist-labels"><span>1898</span><span>1922</span><span>1940</span></div>
</div>
<div class="pp-dsec">
<h3>Richtungsverteilung</h3>
<div class="pp-dsplit"><span class="out">→ 612 ausgehend · 72%</span><span class="in">← 239 eingehend · 28%</span></div>
<div class="pp-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
</div>
<div class="pp-twocol">
<div class="pp-dsec">
<h3>Top Korrespondenten <span class="note">6 von 87</span></h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">Walter Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
<div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
<div class="pp-ti"><span class="nm">Ella Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:42%"></span></span><span class="val">77</span></div>
<div class="pp-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:32%"></span></span><span class="val">58</span></div>
</div>
</div>
<div class="pp-dsec">
<h3>Top Orte <span class="note">5 von 42</span></h3>
<div class="pp-toplist">
<div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
<div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
<div class="pp-ti"><span class="nm">Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
<div class="pp-ti"><span class="nm">Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
<div class="pp-ti"><span class="nm">Belgard</span><span class="bw"><span style="width:6%"></span></span><span class="val">26</span></div>
</div>
</div>
</div>
<div class="pp-dsec">
<h3>Beliebte Schlagwörter <span class="note">Klick filtert den Briefwechsel</span></h3>
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag xl">Familie</span><span class="tag l">Geburtstag</span><span class="tag l">Weihnachten</span><span class="tag m">Kuraufenthalt</span><span class="tag m">Reise</span><span class="tag m">Geschäft</span><span class="tag">Krieg</span><span class="tag muted">Krankheit</span><span class="tag muted">Schule</span><span class="tag muted">Hochzeit</span><span class="tag muted">Neujahr</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="ann">
<strong>Deep-link grammar (unchanged from REV&#8239;1).</strong> Every Rich-tier tile is a link into <code>/briefwechsel</code> with filters applied: histogram bar → <code>?senderId={id}&amp;from={y}-01-01&amp;to={y}-12-31</code> · correspondent row → <code>?senderId={id}&amp;receiverId={other}</code> · location row → <code>?senderId={id}&amp;location={slug}</code> · tag chip → <code>?senderId={id}&amp;tagId={tagId}</code>. Requires new <code>direction</code>, <code>location</code>, <code>tagId</code> query params on <code>/briefwechsel</code>.
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 06 — EMPTY & LOADING
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">06</span>Empty &amp; Loading States</h2>
<p class="sec-intro">New persons (imported but no documents yet) render the Empty state. The Loading state covers the brief window while the dashboard endpoint resolves — person card is already painted from <code>/api/persons/{id}</code> (fast), dashboard slot shows a single skeleton shape sized for the likely tier.</p>
<!-- EMPTY -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">04</span><span class="state-title">Empty — person has no letters yet</span>&nbsp;<span class="tier-badge empty">Empty</span></div>
<div class="state-desc">Single reassurance card. No stats strip (there are no stats). No CTA to the Briefwechsel (it would lead to an empty view). "Bearbeiten" remains on the PersonCard.</div>
<div class="state-vps">
<div class="state-vp-col">
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
<div class="wf wf-m"><div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">FA</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates"></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Sobald ein Brief zugewiesen wird, erscheint der Überblick hier.</div></div></div></div></div></div>
</div>
</div>
<div class="state-vp-col">
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
<div class="wf wf-t"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/new-id</span></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates"></div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.</div></div></div></div></div></div>
</div>
</div>
<div class="state-vp-col">
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
<div class="wf wf-d"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/new-id</span></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates"></div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.</div></div></div></div></div></div>
</div>
</div>
</div>
</div>
<!-- LOADING -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">05</span><span class="state-title">Loading — skeleton sized for the expected tier</span></div>
<div class="state-desc">Dashboard endpoint in flight. Skeleton shows only the bare silhouette — a single rectangle at Compact, a stats+list silhouette at Standard+. Reduced-motion users see a static gradient instead of the shimmer animation.</div>
<div class="state-vps">
<div class="state-vp-col">
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
<div class="wf wf-m"><div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">FA</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div></div></div></div></div></div>
</div>
</div>
<div class="state-vp-col">
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
<div class="wf wf-t"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/…</span></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div><div class="pp-actions"><div class="pp-btn">Edit</div><div class="pp-btn primary">Briefwechsel</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div></div><div class="pp-dsec"><div class="sk" style="height:6px;width:100%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:9px;width:90%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:80%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:65%"></div></div></div></div></div></div>
</div>
</div>
<div class="state-vp-col">
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
<div class="wf wf-d"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/…</span></div></div>
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 1923</div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div></div><div class="pp-dsec"><div class="sk" style="height:72px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:8px;width:100%;margin-bottom:5px"></div><div class="sk" style="height:12px;width:100%"></div></div><div class="pp-twocol"><div class="pp-dsec"><div class="sk" style="height:11px;width:90%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:80%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:65%"></div></div><div class="pp-dsec"><div class="sk" style="height:11px;width:90%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:80%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:65%"></div></div></div></div></div></div></div>
</div>
</div>
</div>
<div class="ann">
<strong>Anti-flicker rule.</strong> If the <code>letterCount</code> from <code>/api/persons/{id}</code> (fast) arrives before <code>/api/persons/{id}/dashboard</code>, the skeleton already knows which tier to paint — so the skeleton shape matches the final rendered shape and the page does not jump when data arrives. Use the count hint eagerly.
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 07 — ACCESSIBILITY
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">07</span>Accessibility Contract · WCAG AA/AAA</h2>
<p class="sec-intro">Every rendered colour pair has been measured in light and dark mode. Semantics are chosen per tier: Compact uses a <code>&lt;dl&gt;</code> stat line; Standard/Rich reuse that and add <code>&lt;ol&gt;</code> for tile lists; Rich adds <code>role="img"</code> on the histogram with a text alternative that carries the shape information.</p>
<div class="impl-ref">
<div class="impl-ref-hdr">Light Mode — Contrast Verification<span>layout.css tokens</span></div>
<table>
<thead><tr><th>Pair</th><th>Value</th><th>Ratio</th><th>WCAG</th></tr></thead>
<tbody>
<tr><td>Stat number (primary on muted)</td><td><code>#002850 on #fafaf5</code></td><td class="ir-px">14.0:1</td><td>AAA ✓</td></tr>
<tr><td>Stat label (ink-3 on muted)</td><td><code>#666666 on #fafaf5</code></td><td class="ir-px">5.4:1</td><td>AA ✓</td></tr>
<tr><td>Stat "in" (accent on muted, 22&#8239;px/900)</td><td><code>#2F9E95 on #fafaf5</code></td><td class="ir-px">4.5:1</td><td>AA ✓ (large text rule)</td></tr>
<tr><td>Dashboard header (surface on primary)</td><td><code>#ffffff on #002850</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
<tr><td>CTA "Briefwechsel öffnen" (primary on accent)</td><td><code>#002850 on #a6dad8</code></td><td class="ir-px">8.1:1</td><td>AAA ✓</td></tr>
<tr><td>Histogram peak bar (primary on surface)</td><td><code>#002850 on #ffffff</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
<tr><td>Tag chip (primary on accent)</td><td><code>#002850 on #a6dad8</code></td><td class="ir-px">8.1:1</td><td>AAA ✓</td></tr>
<tr><td>Muted tag (ink-3 on line)</td><td><code>#666666 on #eee8dc</code></td><td class="ir-px">5.1:1</td><td>AA ✓</td></tr>
<tr><td>Focus ring (primary on surface, 2&#8239;px offset)</td><td><code>#002850</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Dark Mode — Contrast Verification<span>data-theme="dark"</span></div>
<table>
<thead><tr><th>Pair</th><th>Value</th><th>Ratio</th><th>WCAG</th></tr></thead>
<tbody>
<tr><td>Stat number (ink on canvas-2)</td><td><code>#f0efe9 on #011526</code></td><td class="ir-px">15.1:1</td><td>AAA ✓</td></tr>
<tr><td>Stat "out" (mint on canvas-2)</td><td><code>#a1dcd8 on #011526</code></td><td class="ir-px">9.6:1</td><td>AAA ✓</td></tr>
<tr><td>Stat "in" (turquoise on canvas-2)</td><td><code>#00c7b1 on #011526</code></td><td class="ir-px">6.8:1</td><td>AA ✓</td></tr>
<tr><td>Histogram peak (mint on canvas)</td><td><code>#a1dcd8 on #010e1e</code></td><td class="ir-px">9.2:1</td><td>AAA ✓</td></tr>
<tr><td>Tag (turquoise on tint)</td><td><code>#00c7b1 on rgba(0,199,177,.2)</code></td><td class="ir-px">6.3:1</td><td>AA ✓</td></tr>
</tbody>
</table>
</div>
<div class="ann">
<strong>Non-negotiable accessibility rules per tier.</strong>
<ul>
<li><b>All non-Empty tiers (shared chrome).</b> Stats strip is a real <code>&lt;dl&gt;</code> — screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4". The header CTA has an <code>aria-label</code> that includes the person's name ("Briefwechsel von Elsbeth Brandt öffnen").</li>
<li><b>Compact.</b> Nothing renders below the stats strip. No additional semantic concerns beyond the shared chrome.</li>
<li><b>Standard.</b> Adds direction bar as <code>role="img"</code> with <code>aria-label="11 von 18 Briefen ausgehend, 7 eingehend"</code>. Top correspondents are a semantic <code>&lt;ol&gt;</code> — "Top Korrespondenten, 5 Einträge, 1 von 5 Walter de Gruyter, 6 Briefe".</li>
<li><b>Rich.</b> Adds histogram as <code>role="img"</code> with <code>aria-label</code> describing range &amp; peak: "Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen". Tag cloud is a <code>&lt;ul&gt;</code> of <code>&lt;li&gt;&lt;a&gt;</code>; the visual size does <em>not</em> encode meaning beyond the text — count is exposed via <code>title</code> + <code>aria-label</code>.</li>
<li><b>All tiers.</b> Focus ring: <code>focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2</code>. Touch targets: CTA 44&#8239;px, top-list row 44&#8239;px on mobile / 32&#8239;px on desktop, tag chip 44&#8239;px min width, histogram bar 32&#8239;px invisible click target.</li>
<li><b>Reduced motion.</b> Skeleton shimmer collapses to a static gradient; tag hover lift is disabled; histogram bar fade-in on first render is skipped. Honor <code>@media (prefers-reduced-motion: reduce)</code>.</li>
</ul>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 08 — IMPLEMENTATION
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">08</span>Implementation Notes — Tiered API, Lazy Components, Shipping Order</h2>
<div class="impl-ref">
<div class="impl-ref-hdr">Endpoint<span>tier-aware response</span></div>
<table>
<thead><tr><th>Field</th><th>Value</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Route</td><td><code>GET /api/persons/{id}/dashboard</code></td><td>Separate from <code>/api/persons/{id}</code> — lets person entity load stay lean and the dashboard query stay cache-friendly</td></tr>
<tr><td>Permission</td><td><code>@RequirePermission(Permission.READ_ALL)</code></td><td>Same as <code>/api/persons/{id}</code></td></tr>
<tr><td>Cache key</td><td><code>(personId, dataVersion)</code></td><td>Invalidated on any Document write that touches this person (sender or receiver change, tag change, location change)</td></tr>
<tr><td>Expensive aggregations</td><td>Only run when computed <code>tier == RICH</code></td><td><code>activityByYear</code>, <code>topLocations</code>, <code>topTags</code> are <code>null</code> at lower tiers — DB query planner skips those subqueries entirely</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Response schema<span>PersonDashboardDTO — nullable fields by tier</span></div>
<table>
<thead><tr><th>Field</th><th>Type</th><th>Populated at tier</th></tr></thead>
<tbody>
<tr><td><code>tier</code></td><td><code>"EMPTY" | "COMPACT" | "STANDARD" | "RICH"</code></td><td>always · advisory hint; frontend may re-derive</td></tr>
<tr><td><code>totalCount</code></td><td>int</td><td>always</td></tr>
<tr><td><code>outCount</code></td><td>int</td><td>always</td></tr>
<tr><td><code>inCount</code></td><td>int</td><td>always</td></tr>
<tr><td><code>firstLetterYear</code></td><td>int?</td><td>Compact+ (null when totalCount == 0)</td></tr>
<tr><td><code>lastLetterYear</code></td><td>int?</td><td>Compact+</td></tr>
<tr><td><code>yearSpan</code></td><td>int?</td><td>Compact+ · <code>last - first + 1</code></td></tr>
<tr><td><code>correspondentCount</code></td><td>int?</td><td>Standard+</td></tr>
<tr><td><code>topCorrespondents</code></td><td>List&lt;CorrespondentTileDTO&gt;?</td><td>Standard+ · max&#8239;5</td></tr>
<tr><td><code>activityByYear</code></td><td>Map&lt;int,&#8239;int&gt;?</td><td><b>Rich only</b> · contiguous from first to last year</td></tr>
<tr><td><code>peakYear / peakYearCount</code></td><td>int? · int?</td><td>Rich only</td></tr>
<tr><td><code>topLocations</code></td><td>List&lt;LocationTileDTO&gt;?</td><td>Rich only · max&#8239;5</td></tr>
<tr><td><code>topTags</code></td><td>List&lt;TagTileDTO&gt;?</td><td>Rich only · max&#8239;12 · minimum count threshold 3</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Component structure<span>baseline eager · Rich-only lazy</span></div>
<table>
<thead><tr><th>File</th><th>Tier</th><th>Load</th><th>Responsibility</th></tr></thead>
<tbody>
<tr><td><code>PersonDashboard.svelte</code></td><td>all</td><td>eager</td><td>Orchestrator — renders header + stats always, then conditionally appends sections based on <code>tier</code></td></tr>
<tr><td><code>DashboardHeader.svelte</code></td><td>Compact, Standard, Rich</td><td>eager</td><td>Navy strip: title + "↗ Briefwechsel öffnen" CTA · shared across all non-Empty tiers</td></tr>
<tr><td><code>StatStrip.svelte</code></td><td>Compact, Standard, Rich</td><td>eager</td><td>4-cell stats grid with direction colouring · shared across all non-Empty tiers</td></tr>
<tr><td><code>DistributionBar.svelte</code></td><td>Standard, Rich</td><td>eager</td><td><em>Shared</em> with <code>briefwechsel-thumbnail-rows-spec</code> — not new</td></tr>
<tr><td><code>TopTileList.svelte</code></td><td>Standard, Rich</td><td>eager</td><td>Ordered list of tiles (name + bar + count) · generic (used for correspondents and locations)</td></tr>
<tr><td><code>ActivityHistogram.svelte</code></td><td>Rich only</td><td><b>lazy</b></td><td>Dynamic import: <code>{#await import('./ActivityHistogram.svelte')}</code></td></tr>
<tr><td><code>TagCloud.svelte</code></td><td>Rich only</td><td><b>lazy</b></td><td>Dynamic import; size buckets derived from counts</td></tr>
<tr><td><code>EmptyNotice.svelte</code></td><td>Empty</td><td>eager</td><td>Single card with reassurance text</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Route changes<span>/persons/[id] shell</span></div>
<table>
<thead><tr><th>File</th><th>Change</th></tr></thead>
<tbody>
<tr><td><code>+page.server.ts</code></td><td>Add parallel call to <code>/api/persons/{id}/dashboard</code>. Keep existing error handling pattern (<code>result.response.ok</code> check · <code>getErrorMessage(code)</code>).</td></tr>
<tr><td><code>+page.svelte</code></td><td>Replace <code>&lt;CoCorrespondentsList&gt;</code> with <code>&lt;PersonDashboard dashboard={data.dashboard} /&gt;</code>. Remove the local <code>coCorrespondents</code> derivation — backend owns it.</td></tr>
<tr><td><code>CoCorrespondentsList.svelte</code></td><td>Keep temporarily (used nowhere else after this ships). Delete in a follow-up commit once the new route is green in QA.</td></tr>
</tbody>
</table>
</div>
<div class="ann">
<strong>Shipping order.</strong>
<ul>
<li><b>Phase 1</b> — backend endpoint <code>GET /api/persons/{id}/dashboard</code> returning the tiered <code>PersonDashboardDTO</code>. Tests for Empty / Compact / Standard / Rich. Cache keyed on person data-version. Skip expensive aggregations for non-Rich persons.</li>
<li><b>Phase 2</b><code>PersonDashboard.svelte</code> + <code>DashboardHeader.svelte</code> + <code>StatStrip.svelte</code> + <code>EmptyNotice.svelte</code>. Replaces <code>CoCorrespondentsList</code> in the right column. <b>This phase alone ships the Compact tier — value for ~90&#8239;% of persons.</b></li>
<li><b>Phase 3</b><code>TopTileList.svelte</code> + reuse <code>DistributionBar.svelte</code>. Standard tier's direction bar and top-correspondents tiles go live for ~6&#8239;% of persons.</li>
<li><b>Phase 4</b><code>ActivityHistogram.svelte</code>, <code>TagCloud.svelte</code> (both lazy). Wire new <code>direction</code>, <code>location</code>, <code>tagId</code> query params on <code>/briefwechsel</code>. Rich tier goes live for the 4 power-correspondence persons.</li>
<li><b>Phase 5</b> — axe-playwright checks at 320 / 768 / 1440 in light + dark for each tier. Visual regression snapshots for all five states (Empty · Compact · Standard · Rich · Loading).</li>
<li><b>Phase 6 (cleanup)</b> — delete <code>CoCorrespondentsList.svelte</code> and any now-unused co-correspondent derivation helpers.</li>
</ul>
</div>
<div class="ann" style="background:#F0FDF4;border-color:#86EFAC;color:#14532D">
<strong style="color:#166534">Migration story.</strong> The old spec's <code>PersonDashboard</code> (six eager blocks) has not shipped yet — there is no user-facing migration. The existing <code>CoCorrespondentsList</code> is what each tier replaces. Compact and Standard tiers are strict improvements over <code>CoCorrespondentsList</code>; Rich tier is the only place the original six-block vision survives, and it only renders for persons who demonstrably have the data to fill it.
</div>
</section>
</div>
</body>
</html>