Files
familienarchiv/docs/specs/person-dashboard-spec.html
Marcel dd6cbe3a6f
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m41s
CI / OCR Service Tests (push) Successful in 33s
CI / Backend Unit Tests (push) Failing after 2m48s
docs(specs): add final specs for thumbnail rows + person dashboard
Two production-ready specs following the chronik-spec format
(scaled wireframes × 3 viewports + impl-ref tables with exact Tailwind
classes and pixel values + WCAG contrast verification):

- briefwechsel-thumbnail-rows-spec.html — /briefwechsel row redesign
  with PDF thumbnail, summary-as-quote, bilateral distribution bar;
  drops status lifecycle and script-type indicators.

- person-dashboard-spec.html — new Korrespondenz-Überblick block on
  /persons/[id] with stats, activity histogram, direction split, top
  correspondents/locations, tag cloud. Every tile deep-links to
  /briefwechsel with filters.

Both specs share the DistributionBar.svelte component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 20:17:21 +02:00

1044 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 · Final Design Spec · Familienarchiv</title>
<style>
/* ═══════════════════════════════════════════════════════════
PERSON DASHBOARD — /persons/[id] extension
Adds "Korrespondenz-Überblick" block to the right column:
stats, activity histogram, direction split, top correspondents,
top locations, tag cloud. Every element deep-links into /briefwechsel.
By Leonie Voss (UX/Design). 2026-04-22.
═══════════════════════════════════════════════════════════ */
*,*::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,.55);max-width:780px;line-height:1.7}
.mast p code{background:rgba(255,255,255,.1);padding:1px 5px;border-radius:2px;font-family:monospace}
.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}
/* ── 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:780px;margin-bottom:24px}
/* ── 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}
.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}
.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 */
.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}
/* Right: dashboard */
.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 */
.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}
/* Histogram */
.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}
/* 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}
/* Two-col arrangement in dashboard */
.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}
/* Cloud */
.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}
/* 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}}
/* ── Close-ups at ~100% ─────────────────────── */
.cu{background:#fff;border:1px solid #e4e2d7;border-radius:6px;padding:20px 24px;margin-bottom:20px}
.cu-t{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#666;margin-bottom:14px}
/* Close-up dashboard parts */
.cu-hist{display:flex;align-items:flex-end;gap:2px;height:110px;padding:6px 0 0;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:16px 20px 0}
.cu-hist .bar{flex:1;background:#a1dcd8;opacity:.65;border-radius:2px 2px 0 0;cursor:pointer}
.cu-hist .bar.peak{background:#012851;opacity:.9}
.cu-hist-labels{display:flex;justify-content:space-between;font-size:10px;color:#888;margin-top:6px;font-weight:700;padding:0 20px 14px}
.cu-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:#e4e2d7;border-radius:4px;overflow:hidden;border:1px solid #e4e2d7}
.cu-stats div{background:#fafaf5;padding:18px 14px;text-align:center}
.cu-stats .v{font-family:Georgia,serif;font-size:28px;font-weight:900;color:#012851;letter-spacing:-.5px}
.cu-stats .v.out{color:#012851}
.cu-stats .v.in{color:#2F9E95}
.cu-stats .k{font-size:11px;color:#888;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
.cu-toplist{display:flex;flex-direction:column;gap:8px;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
.cu-ti{display:flex;align-items:center;gap:12px;font-size:13px;padding:5px 6px;border-radius:3px;cursor:pointer}
.cu-ti:hover{background:#f7f5f2}
.cu-ti .nm{flex:1;color:#012851;font-weight:600}
.cu-ti .bw{width:160px;height:7px;background:#f0ede5;border-radius:4px;overflow:hidden;flex-shrink:0}
.cu-ti .bw span{display:block;height:100%;background:#012851;border-radius:4px}
.cu-ti .val{width:38px;text-align:right;font-size:12px;color:#888;font-weight:700;font-variant-numeric:tabular-nums}
.cu-cloud{display:flex;flex-wrap:wrap;gap:6px;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
.cu-cloud .tag{padding:4px 12px;background:#a1dcd8;color:#012851;font-weight:700;border-radius:14px;font-size:12px;cursor:pointer}
.cu-cloud .tag.xl{font-size:15px;padding:5px 14px}
.cu-cloud .tag.l{font-size:13px}
.cu-cloud .tag.muted{background:#EEE8DC;color:#666}
.cu-dsplit{background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
.cu-dsplit-labels{display:flex;justify-content:space-between;font-size:14px;font-weight:700;margin-bottom:8px}
.cu-dsplit-labels .out{color:#012851}
.cu-dsplit-labels .in{color:#2F9E95}
.cu-dbar{height:10px;display:flex;border-radius:5px;overflow:hidden;background:#e4e2d7}
.cu-dbar .out{background:#012851}
.cu-dbar .in{background:#2F9E95}
/* 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</h1>
<p>Final design for the dashboard block that extends <code>/persons/[id]</code>. Gives every person page a correspondence-at-a-glance view — stats, activity per year, direction split, top correspondents, top locations, tag cloud — and turns each element into a filter shortcut into <code>/briefwechsel</code>. Replaces the current <code>CoCorrespondentsList</code> block.</p>
</div>
<div class="mast-badge">FINAL</div>
</div>
<div class="decisions">
<div class="dec"><div class="dec-label">Route</div><div class="dec-value">/persons/[id] · right column</div></div>
<div class="dec"><div class="dec-label">Layout</div><div class="dec-value">35% / 65% split · stacks below 768&#8239;px</div></div>
<div class="dec"><div class="dec-label">Sections shown</div><div class="dec-value">Stats · Histogram · Direction · Correspondents · Locations · Tags</div></div>
<div class="dec"><div class="dec-label">Deep links</div><div class="dec-value">Every tile → /briefwechsel with filters applied</div></div>
</div>
</header>
<div class="spec-disclaimer">
<strong>Reading this spec.</strong> Mockups in Section&#8239;02 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 class + pixel value. Close-ups in Section&#8239;03 render each dashboard block at ~100&#8239;% scale for pixel-accurate reference.
</div>
<!-- TOC -->
<div class="toc">
<div class="toc-t">Inhalt</div>
<ol>
<li><b>01</b> Page anatomy <span>default · 1440&#8239;px</span></li>
<li><b>02</b> Content states × 3 viewports <span>4 states · 12 frames</span></li>
<li><b>03</b> Dashboard block close-ups <span>6 blocks @ real size</span></li>
<li><b>04</b> Deep-link grammar <span>every tile → /briefwechsel</span></li>
<li><b>05</b> Accessibility contract <span>WCAG AA/AAA</span></li>
<li><b>06</b> Implementation notes <span>backend API · components</span></li>
</ol>
</div>
<!-- ═══════════════════════════════════════════════════════════
SECTION 01 — PAGE ANATOMY
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">01</span>Page Anatomy — Default State at 1440&#8239;px</h2>
<p class="sec-intro">The page is a 35% / 65% split (existing <code>lg:grid-cols-[35%_65%]</code>). Left column keeps <code>PersonCard</code> and <code>NameHistoryCard</code>. Right column replaces <code>CoCorrespondentsList</code> with the new <code>PersonDashboard</code> block at the top, followed by the existing sent/received document lists.</p>
<div style="display:grid;grid-template-columns:1fr 300px;gap:32px;align-items:flex-start">
<div>
<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/{id}</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">
<!-- A · Person card -->
<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>
<!-- B · Dashboard -->
<div class="pp-dash">
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel</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 über die Jahre <span class="note">Spitzenjahr <b>1922 · 78</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 · 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 Korrespondenten <span class="note">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 v. 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">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 class="callout-grid">
<div class="cg"><div class="cg-t">A · Person card</div><div class="cg-b">Unchanged in this spec. Avatar, full name, lifespan, bearbeiten + <b>Briefwechsel</b> buttons. The primary action moves from "Edit" to "Open Briefwechsel" because that is what users actually do next.</div></div>
<div class="cg"><div class="cg-t">B · Korrespondenz-Überblick</div><div class="cg-b">New dashboard block. Dark navy header strip with "Briefwechsel öffnen" CTA on the right. Stats strip below header, then 4 sections separated by 1&#8239;px rules.</div></div>
<div class="cg"><div class="cg-t">Stats strip</div><div class="cg-b">4 cells at desktop / 2×2 at tablet / 4 stacked at mobile. Numbers in serif (Merriweather Black) at 22&#8239;px. Direction colours match row border colours elsewhere (out = navy, in = accent).</div></div>
<div class="cg"><div class="cg-t">Activity histogram</div><div class="cg-b">One bar per year in the range (1898 → 1940 = 43 bars). Peak bar uses <code>bg-primary</code>, others <code>bg-accent/60</code>. Hovering a bar shows "{year} · {count} Briefe" tooltip; clicking filters <code>/briefwechsel?senderId=…&amp;from=YYYY-01-01&amp;to=YYYY-12-31</code>.</div></div>
<div class="cg"><div class="cg-t">Top correspondents</div><div class="cg-b">Up to 6 rows. Name + proportional bar + count. Click opens <code>/briefwechsel?senderId=&lt;this&gt;&amp;receiverId=&lt;other&gt;</code> (bilateral view). "Alle N Korrespondenten →" link below.</div></div>
<div class="cg"><div class="cg-t">Top locations &amp; Tag cloud</div><div class="cg-b">Location tiles mirror correspondents. Tag cloud sized by frequency — <code>xl</code> &gt; 100, <code>l</code> &gt; 50, <code>m</code> &gt; 20, muted ≤ 20. Click on any tag: <code>/briefwechsel?senderId=&lt;this&gt;&amp;tag=&lt;id&gt;</code>.</div></div>
</div>
</div>
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Page Shell<span>extends existing /persons/[id]</span></div>
<table>
<thead><tr><th>Element</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Page container</td><td><code>mx-auto max-w-6xl px-4 py-10</code></td><td class="ir-px">max 72rem</td><td>Unchanged · existing route shell</td></tr>
<tr><td>2-column grid</td><td><code>lg:grid lg:grid-cols-[35%_65%] lg:gap-8</code></td><td class="ir-px">32&#8239;px gap</td><td>Existing · unchanged</td></tr>
<tr><td>Left column stack</td><td><code>PersonCard → NameHistoryCard (mt-6)</code></td><td class="ir-px">24&#8239;px mt</td><td>Existing · unchanged</td></tr>
<tr><td>Right column</td><td><code>PersonDashboard → PersonDocumentList(sent) → PersonDocumentList(received)</code></td><td class="ir-px">new + existing</td><td><code>PersonDashboard</code> replaces <code>CoCorrespondentsList</code></td></tr>
<tr><td>Dashboard 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 card pattern from CLAUDE.md</td></tr>
<tr><td>Dashboard header</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 strip — sets dashboard apart from body card patterns</td></tr>
<tr><td>Dashboard title</td><td><code>font-serif text-base font-bold</code></td><td class="ir-px">16&#8239;px / 700</td><td>Merriweather</td></tr>
<tr><td>"Briefwechsel öffnen" CTA</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</code></td><td class="ir-px">44&#8239;px min</td><td>WCAG 2.2 AA touch target; Paraglide <code>m.person_open_conversation()</code></td></tr>
</tbody>
</table>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 02 — CONTENT STATES × 3 VIEWPORTS
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">02</span>Content States &times; 3 Viewports</h2>
<p class="sec-intro">Four states. Every frame renders the page shell (header → back link → split grid). Reading order per state: 320&#8239;px → 768&#8239;px → 1440&#8239;px. At 320 and 768, the grid stacks and the dashboard flows below the person card.</p>
<!-- ══════════════════ STATE 01 · DEFAULT ═════════════ -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">01</span><span class="state-title">Default · Full dataset (851 letters · 42 years · 87 correspondents)</span></div>
<div class="state-desc">The happy path. Every section has data. Numbers are formatted with German number formatting; year range in histogram auto-fits to data span; top lists truncate to 5 on tablet, 6 on desktop, 3 on mobile.</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/9ed8…</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 class="pp-ti"><span class="nm">G. 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</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/9ed8fd47-…</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 class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:26%"></span></span><span class="val">47</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>Wrap behaviour by viewport.</strong>
<ul>
<li>320&#8239;px: grid stacks. Person card then dashboard. Stats drop to 2×2 cells. Histogram shows aggregated year-groups (6 bars), not individual years. Top lists truncate to 3. Location section collapses.</li>
<li>768&#8239;px: grid stacks. 4×1 stats. Full histogram. Two-col section shows correspondents &amp; locations side-by-side. Tag cloud shows 89 tags.</li>
<li>1440&#8239;px: 35/65 split. All sections fully rendered. Tag cloud shows all significant tags (those with count ≥ 5) plus up to 5 muted smaller ones.</li>
</ul>
</div>
</div>
<!-- ══════════════════ STATE 02 · EMPTY ═══════════════ -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">02</span><span class="state-title">Empty · Person has no letters yet</span></div>
<div class="state-desc">Person exists (imported, created manually) but no documents reference them as sender or receiver. Dashboard collapses to a single reassurance card inviting the user to upload.</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">Diese Person hat noch keine Korrespondenz im Archiv.</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 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>
<!-- ══════════════════ STATE 03 · SPARSE ═══════════════ -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">03</span><span class="state-title">Sparse · Few letters (&lt; 10)</span></div>
<div class="state-desc">Person has between 1 and 9 letters. Histogram would be uninformative; it collapses to a single line with year range. Top lists show whatever is present — no "von N" note. No tag cloud unless at least 3 different tags exist.</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">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 1963</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">7</div><div class="k">ges.</div></div><div><div class="v out">4</div><div class="k"></div></div><div><div class="v in">3</div><div class="k"></div></div><div><div class="v">3J</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:5px"><span>1919 1922</span></div></div><div class="pp-dsec"><h3>Top</h3><div class="pp-toplist"><div class="pp-ti"><span class="nm">W. de Gruyter</span><span class="val">4</span></div><div class="pp-ti"><span class="nm">H. Cram</span><span class="val">3</span></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/elsbeth</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">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>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div><div class="pp-stats"><div><div class="v">7</div><div class="k">gesamt</div></div><div><div class="v out">4</div><div class="k">ausgehend</div></div><div><div class="v in">3</div><div class="k">eingehend</div></div><div><div class="v">3</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:9px"><span>1919 · erster Brief</span><span>1922 · letzter Brief</span></div></div><div class="pp-dsec"><h3>Korrespondenten</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">4</span></div><div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:75%"></span></span><span class="val">3</span></div></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/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></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 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">7</div><div class="k">Briefe gesamt</div></div><div><div class="v out">4</div><div class="k">ausgehend</div></div><div><div class="v in">3</div><div class="k">eingehend</div></div><div><div class="v">3</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:11px"><span style="color:#012851">1919 · erster Brief</span><span style="color:#012851">1922 · letzter Brief</span></div></div><div class="pp-twocol"><div class="pp-dsec"><h3>Korrespondenten</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">4</span></div><div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:75%"></span></span><span class="val">3</span></div></div></div><div class="pp-dsec"><h3>Orte</h3><div class="pp-toplist"><div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:100%"></span></span><span class="val">5</span></div><div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:40%"></span></span><span class="val">2</span></div></div></div></div></div></div></div></div>
</div>
</div>
</div>
<div class="ann"><strong>Sparse-mode simplifications.</strong>
<ul>
<li>Histogram replaced with a single "Zeitraum" line showing first and last letter years. Histogram returns as soon as <code>letterCount &gt;= 10</code> and <code>yearSpan &gt;= 3</code>.</li>
<li>Tag cloud hidden when fewer than 3 distinct tags across all letters. No "empty tag cloud" placeholder.</li>
<li>"Top of N" notes dropped — numbers speak for themselves.</li>
</ul>
</div>
</div>
<!-- ══════════════════ STATE 04 · LOADING ═══════════════ -->
<div class="state-block">
<div class="state-hdr"><span class="state-num">04</span><span class="state-title">Loading · Skeleton</span></div>
<div class="state-desc">While the aggregation endpoint is in-flight. Person card renders from the <code>/api/persons/{id}</code> payload (fast); the dashboard shows skeleton rectangles for each section in the same grid slots.</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 class="pp-dsec"><div class="sk" style="height:18px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:5px;width:80%;margin-bottom:3px"></div><div class="sk" style="height:5px;width:70%;margin-bottom:3px"></div><div class="sk" style="height:5px;width:50%"></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:50px;width:100%"></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>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 03 — DASHBOARD BLOCK CLOSE-UPS
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">03</span>Dashboard Block Close-Ups · ~100% Scale</h2>
<p class="sec-intro">Six blocks rendered at near-real pixel sizes. These are the reference renderings developers check against when implementing <code>PersonDashboard.svelte</code> and its sub-components.</p>
<!-- Block 1: Stats strip -->
<div class="cu">
<div class="cu-t">Block A · Stats strip</div>
<div class="cu-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="impl-ref">
<div class="impl-ref-hdr">Block A — Stats Strip<span>Desktop = 4 cells · Tablet = 4 cells · Mobile = 2×2</span></div>
<table>
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Strip container</td><td><code>grid grid-cols-2 sm:grid-cols-4 gap-px bg-line border-b border-line</code></td><td class="ir-px">1&#8239;px gap (shows as lines)</td><td>Separators are the background showing through</td></tr>
<tr><td>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 separators read</td></tr>
<tr><td>Number</td><td><code>font-serif text-[22px] font-black text-primary 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</td></tr>
<tr><td>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>Direction labels match number colour</td></tr>
<tr><td>Mobile formatter</td><td>Abbreviate "Briefe gesamt" → "gesamt"</td><td class="ir-px">m.person_stats_total_short()</td><td>Paraglide key with <code>_short</code> suffix</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Block 2: Activity histogram -->
<div class="cu">
<div class="cu-t">Block B · Activity histogram (one bar per year)</div>
<div class="cu-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="cu-hist-labels"><span>1898</span><span>1922 ▲ Spitzenjahr · 78 Briefe</span><span>1940</span></div>
<div class="impl-ref">
<div class="impl-ref-hdr">Block B — Activity Histogram<span>deep-links via from/to params</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>flex items-end gap-0.5 h-[72px] pt-1</code></td><td class="ir-px">72&#8239;px height</td><td>Height tuned to comfortable reading — not too small</td></tr>
<tr><td>Bar (normal)</td><td><code>flex-1 bg-accent/60 rounded-t-sm hover:bg-accent/90 transition-colors cursor-pointer</code></td><td class="ir-px">variable width</td><td>Min-width 3&#8239;px for ≤ 30 years; thinner for longer ranges</td></tr>
<tr><td>Bar (peak)</td><td><code>flex-1 bg-primary rounded-t-sm</code></td><td class="ir-px">max height</td><td>Exactly one peak bar highlighted</td></tr>
<tr><td>Bar link</td><td><code>&lt;a href="/briefwechsel?senderId={id}&amp;from={year}-01-01&amp;to={year}-12-31"&gt;</code></td><td class="ir-px"></td><td>Whole bar is the link; tooltip announces "Jahr {year} · {count} Briefe"</td></tr>
<tr><td>Year labels</td><td><code>flex justify-between text-[10px] font-bold text-ink-3 mt-1.5</code></td><td class="ir-px">10&#8239;px</td><td>Only show: earliest year · peak year · latest year</td></tr>
<tr><td>Tooltip</td><td>native <code>title</code> attribute on bar</td><td class="ir-px"></td><td>"1922 · 78 Briefe" — Paraglide pluralized</td></tr>
<tr><td>Empty-year bars</td><td>rendered as <code>min-height: 2px</code> placeholder</td><td class="ir-px">2&#8239;px</td><td>Keeps bar spacing regular across decades with gaps</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Block 3: Direction split -->
<div class="cu">
<div class="cu-t">Block C · Direction split (same component as /briefwechsel distribution bar, re-used)</div>
<div class="cu-dsplit">
<div class="cu-dsplit-labels"><span class="out">→ 612 ausgehend · 72%</span><span class="in">← 239 eingehend · 28%</span></div>
<div class="cu-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
</div>
<div class="ann"><strong>Re-use, do not duplicate.</strong> The bilateral <code>DistributionBar.svelte</code> component from the thumbnail rows spec is also used here. Same props (<code>outCount</code>, <code>outLabel</code>, <code>inCount</code>, <code>inLabel</code>), same aria-label pattern.</div>
</div>
<!-- Block 4: Top correspondents -->
<div class="cu">
<div class="cu-t">Block D · Top correspondents</div>
<div class="cu-toplist">
<div class="cu-ti"><span class="nm">Walter Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
<div class="cu-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
<div class="cu-ti"><span class="nm">Ella Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
<div class="cu-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="cu-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:32%"></span></span><span class="val">58</span></div>
<div class="cu-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:26%"></span></span><span class="val">47</span></div>
</div>
<div class="impl-ref">
<div class="impl-ref-hdr">Block D — Top List (correspondents &amp; locations share this)<span>rendered as &lt;ol&gt; for semantic ranking</span></div>
<table>
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Wrapper</td><td><code>&lt;ol&gt;</code> · <code>flex flex-col gap-2</code></td><td class="ir-px">8&#8239;px gap</td><td>Ordered list — rank order matters, screen readers announce "1 of 6"</td></tr>
<tr><td>Item</td><td><code>&lt;li&gt;</code> containing <code>&lt;a&gt;</code> · <code>flex items-center gap-3 text-sm px-1.5 py-1 rounded-sm min-h-[32px] hover:bg-muted</code></td><td class="ir-px">32&#8239;px min</td><td>Not 44&#8239;px because these are secondary links inside a card — desktop focus. Mobile bumps to 44 via <code>md:min-h-[32px] min-h-[44px]</code></td></tr>
<tr><td>Name</td><td><code>flex-1 font-semibold text-ink truncate</code></td><td class="ir-px">14&#8239;px / 600</td><td>Truncate middle-ellipsis on very long German names at &lt; 768&#8239;px</td></tr>
<tr><td>Proportional bar wrapper</td><td><code>w-[120px] h-[7px] bg-line rounded overflow-hidden shrink-0 hidden sm:block</code></td><td class="ir-px">120 × 7</td><td>Hidden on mobile — the number carries the data</td></tr>
<tr><td>Proportional bar fill</td><td><code>h-full bg-primary rounded</code></td><td class="ir-px">width = value ÷ max</td><td>Widths computed client-side from the list's max value</td></tr>
<tr><td>Count</td><td><code>w-10 text-right text-sm text-ink-3 font-bold tabular-nums shrink-0</code></td><td class="ir-px">14&#8239;px / 700</td><td>Tabular figures so columns align</td></tr>
<tr><td>"Alle N anzeigen →"</td><td><code>mt-2.5 text-xs font-bold text-primary border-b border-dashed border-primary/60 hover:border-primary</code></td><td class="ir-px">12&#8239;px / 700</td><td>Appears when total &gt; shown count</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Block 5: Top locations -->
<div class="cu">
<div class="cu-t">Block E · Top locations — identical component to Block D</div>
<div class="cu-toplist">
<div class="cu-ti"><span class="nm">📍 Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
<div class="cu-ti"><span class="nm">📍 B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
<div class="cu-ti"><span class="nm">📍 Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
<div class="cu-ti"><span class="nm">📍 Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
<div class="cu-ti"><span class="nm">📍 Belgard</span><span class="bw"><span style="width:6%"></span></span><span class="val">26</span></div>
</div>
<div class="ann"><strong>Emoji pin is decorative.</strong> Rendered via <code>::before</code> with <code>aria-hidden="true"</code>. The location string alone carries semantic meaning. Future iteration may replace with a map icon component.</div>
</div>
<!-- Block 6: Tag cloud -->
<div class="cu">
<div class="cu-t">Block F · Tag cloud — frequency-sized</div>
<div class="cu-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">Kuraufenthalt</span><span class="tag">Reise</span><span class="tag">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 class="impl-ref">
<div class="impl-ref-hdr">Block F — Tag Cloud<span>size buckets map to count thresholds</span></div>
<table>
<thead><tr><th>Size</th><th>Classes</th><th>Count threshold</th><th>Note</th></tr></thead>
<tbody>
<tr><td>xl</td><td><code>text-[15px] font-bold px-3.5 py-1 bg-accent text-primary rounded-full</code></td><td class="ir-px">≥ 100</td><td>Max 2 tags at this size — visual anchor</td></tr>
<tr><td>l</td><td><code>text-[13px] font-bold px-3 py-1 bg-accent text-primary rounded-full</code></td><td class="ir-px">5099</td><td>Up to 4 tags</td></tr>
<tr><td>m</td><td><code>text-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full</code></td><td class="ir-px">2049</td><td>Up to 6 tags</td></tr>
<tr><td>regular</td><td><code>text-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full</code></td><td class="ir-px">519</td><td>All tags meeting threshold</td></tr>
<tr><td>muted</td><td><code>text-xs font-semibold px-2.5 py-0.5 bg-line text-ink-3 rounded-full</code></td><td class="ir-px">&lt; 5</td><td>Up to 8 muted tags, sorted alphabetically</td></tr>
<tr><td>Click target</td><td><code>min-h-[28px] min-w-[44px] inline-flex items-center</code></td><td class="ir-px">44&#8239;px min width</td><td>Very short tag names ("Kur") still meet touch target</td></tr>
<tr><td>Hover</td><td><code>hover:-translate-y-px transition-transform</code></td><td class="ir-px">1&#8239;px lift</td><td>Bypassed when <code>prefers-reduced-motion</code></td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 04 — DEEP-LINK GRAMMAR
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">04</span>Deep-Link Grammar — every tile → /briefwechsel</h2>
<p class="sec-intro">The dashboard is the discovery surface; <code>/briefwechsel</code> is the reading surface. Every clickable element in the dashboard adds filters to the existing <code>/briefwechsel</code> query string so the transition feels continuous.</p>
<div class="impl-ref">
<div class="impl-ref-hdr">Link grammar<span>replace placeholders with real values</span></div>
<table>
<thead><tr><th>Element</th><th>Link</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Header "↗ Briefwechsel öffnen"</td><td><code>/briefwechsel?senderId={id}&amp;dir=DESC</code></td><td>Opens all letters for this person · no date filter · newest first</td></tr>
<tr><td>Stats — "gesamt"</td><td>Same as header</td><td>Entire count is tap-to-open</td></tr>
<tr><td>Stats — "ausgehend"</td><td><code>/briefwechsel?senderId={id}&amp;direction=OUT&amp;dir=DESC</code></td><td>Requires new <code>direction</code> query param on /briefwechsel</td></tr>
<tr><td>Stats — "eingehend"</td><td><code>/briefwechsel?senderId={id}&amp;direction=IN&amp;dir=DESC</code></td><td>Same</td></tr>
<tr><td>Histogram bar</td><td><code>/briefwechsel?senderId={id}&amp;from={year}-01-01&amp;to={year}-12-31</code></td><td>Opens bilateral or single view scoped to that year</td></tr>
<tr><td>Top correspondent row</td><td><code>/briefwechsel?senderId={id}&amp;receiverId={otherId}&amp;dir=DESC</code></td><td>Opens the bilateral view for the pair</td></tr>
<tr><td>Top location row</td><td><code>/briefwechsel?senderId={id}&amp;location={locSlug}</code></td><td>Requires new <code>location</code> query param on /briefwechsel</td></tr>
<tr><td>Tag chip</td><td><code>/briefwechsel?senderId={id}&amp;tagId={tagId}</code></td><td>Requires new <code>tagId</code> query param on /briefwechsel</td></tr>
</tbody>
</table>
</div>
<div class="ann"><strong>New query parameters required on /briefwechsel.</strong>
<ul>
<li><code>direction=OUT|IN</code> — filter to letters in one direction only (existing endpoint already distinguishes sender vs receiver; this adds symmetry for the single-person view).</li>
<li><code>location=&lt;slug&gt;</code> — case-insensitive match on <code>Document.location</code>. Slug because German locations can contain spaces and dots ("B.Lichterfelde", "Bad Kissingen").</li>
<li><code>tagId=&lt;uuid&gt;</code> — filter to letters that reference this tag. If omitted, no tag filter is applied.</li>
</ul>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 05 — ACCESSIBILITY
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">05</span>Accessibility Contract · WCAG AA/AAA</h2>
<p class="sec-intro">Every colour pair on the dashboard has been measured. Semantics matter as much as colour: the stats use real numbers (not SVG), the histogram has tooltip text, the top lists are ordered lists, the tag cloud is a list of links.</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 #f7f5f2</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 #f7f5f2</code></td><td class="ir-px">5.4:1</td><td>AA ✓</td></tr>
<tr><td>Stat "in" (accent on muted)</td><td><code>#2F9E95 on #f7f5f2</code></td><td class="ir-px">4.5:1</td><td>AA ✓ (borderline — number is 22&#8239;px / 900 qualifies as large)</td></tr>
<tr><td>Dashboard header title (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 bar (accent/60 on surface)</td><td><code>rgba(47,158,149,.6) on #ffffff</code></td><td class="ir-px">2.8:1</td><td>Decorative (bars have titles; not text)</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>Stat label (ink-3 on canvas-2)</td><td><code>#8b97a5 on #011526</code></td><td class="ir-px">7.1:1</td><td>AAA ✓</td></tr>
<tr><td>Dashboard header (ink on navy-2)</td><td><code>#f0efe9 on #01223f</code></td><td class="ir-px">13.8:1</td><td>AAA ✓</td></tr>
<tr><td>CTA (primary on mint)</td><td><code>#012851 on #a1dcd8</code></td><td class="ir-px">9.6:1</td><td>AAA ✓</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.</strong>
<ul>
<li>Stats are real <code>&lt;dl&gt;</code> / <code>&lt;dt&gt;</code> / <code>&lt;dd&gt;</code> pairs. Screen readers announce "Briefe gesamt: 851, ausgehend: 612, …".</li>
<li>Histogram wrapped in <code>role="img"</code> with <code>aria-label="Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen"</code>. Each bar has a <code>title</code> attribute for sighted tooltip.</li>
<li>Top lists are semantic <code>&lt;ol&gt;</code> elements — screen readers announce "Top Korrespondenten, list 6 items, 1 of 6 Walter Dieckmann, 184 Briefe".</li>
<li>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> carry meaning that text cannot — the count is not exposed to screen readers via size, only via the tooltip / aria-label "Schlagwort Verlag, {count} Briefe".</li>
<li>Focus ring: <code>focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2</code> on every interactive element (histogram bars, top-list rows, tag chips, header CTA).</li>
<li>Touch targets: CTA 44&#8239;px, histogram bar 32&#8239;px min width (padded invisibly if needed), top-list row 44&#8239;px on mobile / 32&#8239;px on desktop, tag chip 44&#8239;px min width.</li>
<li><code>prefers-reduced-motion</code>: disable bar width animation on first render and the tag-chip hover lift. The skeleton shimmer collapses to a static gradient.</li>
</ul>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════
SECTION 06 — IMPLEMENTATION NOTES
═══════════════════════════════════════════════════════════ -->
<section class="sec">
<h2 class="sec-h"><span class="sec-num">06</span>Implementation Notes — Backend + Components</h2>
<div class="impl-ref">
<div class="impl-ref-hdr">New backend endpoint<span>aggregation for dashboard</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> to keep person entity lean and let the dashboard query be 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</td><td>Server-side cache keyed on <code>(personId, dataVersion)</code></td><td>Invalidate on Document write that references this person (see <code>DocumentService</code> update hooks)</td></tr>
<tr><td>Response schema</td><td>see next table</td><td>All counts server-computed — never client-computed</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Response schema<span>PersonDashboardDTO</span></div>
<table>
<thead><tr><th>Field</th><th>Type</th><th>Note</th></tr></thead>
<tbody>
<tr><td><code>totalCount</code></td><td>int</td><td>outCount + inCount</td></tr>
<tr><td><code>outCount</code></td><td>int</td><td>Letters where this person is sender</td></tr>
<tr><td><code>inCount</code></td><td>int</td><td>Letters where this person is in receivers</td></tr>
<tr><td><code>yearSpan</code></td><td>int</td><td>latestYear - earliestYear + 1 (null when no letters)</td></tr>
<tr><td><code>correspondentCount</code></td><td>int</td><td>Distinct counterparts</td></tr>
<tr><td><code>activityByYear</code></td><td>Map&lt;int, int&gt;</td><td>year → count · always contiguous (missing years = 0 · dashboard decides display)</td></tr>
<tr><td><code>peakYear</code></td><td>int</td><td>Year with most letters (null when no letters)</td></tr>
<tr><td><code>peakYearCount</code></td><td>int</td><td></td></tr>
<tr><td><code>topCorrespondents</code></td><td>List&lt;CorrespondentTileDTO&gt;</td><td>Max 10 · sorted desc · each has <code>personId · displayName · count</code></td></tr>
<tr><td><code>topLocations</code></td><td>List&lt;LocationTileDTO&gt;</td><td>Max 10 · each has <code>location · count</code></td></tr>
<tr><td><code>topTags</code></td><td>List&lt;TagTileDTO&gt;</td><td>Max 20 · each has <code>tagId · label · count</code> · frontend buckets into size tiers</td></tr>
</tbody>
</table>
</div>
<div class="impl-ref" style="margin-top:16px">
<div class="impl-ref-hdr">Component structure<span>new files + changes</span></div>
<table>
<thead><tr><th>File</th><th>Responsibility</th><th>Change</th></tr></thead>
<tbody>
<tr><td><code>PersonDashboard.svelte</code></td><td>Orchestrator · renders header + stats + sections</td><td>new</td></tr>
<tr><td><code>StatStrip.svelte</code></td><td>4-cell stats grid with direction colouring</td><td>new</td></tr>
<tr><td><code>ActivityHistogram.svelte</code></td><td>One bar per year, peak highlight, hover tooltip</td><td>new</td></tr>
<tr><td><code>DistributionBar.svelte</code></td><td>Already introduced in <code>briefwechsel-thumbnail-rows-spec.html</code> · re-used here with different labels</td><td>shared</td></tr>
<tr><td><code>TopTileList.svelte</code></td><td>Ordered list of tiles (name + bar + count) — used for correspondents and locations</td><td>new · generic</td></tr>
<tr><td><code>TagCloud.svelte</code></td><td>Frequency-sized chips with size buckets</td><td>new</td></tr>
<tr><td><code>PersonPageShell.svelte</code> / <code>+page.svelte</code></td><td>Renders 2-column grid</td><td>Replace <code>CoCorrespondentsList</code> with <code>PersonDashboard</code>. Remove <code>coCorrespondents</code> derivation in <code>+page.svelte</code> — dashboard owns it.</td></tr>
<tr><td><code>+page.server.ts</code></td><td>Loads person data</td><td>Add parallel call to <code>/api/persons/{id}/dashboard</code>; keep error handling identical</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> with <code>PersonDashboardDTO</code>. Cache on person-write hooks. Tests for empty / sparse / full.</li>
<li><b>Phase 2</b><code>PersonDashboard.svelte</code> + its six sub-components. Replaces <code>CoCorrespondentsList</code> in the right column.</li>
<li><b>Phase 3</b> — new query params on <code>/briefwechsel</code>: <code>direction</code>, <code>location</code>, <code>tagId</code>. Wire every dashboard element to them.</li>
<li><b>Phase 4</b> — axe-playwright tests at 320 / 768 / 1440 in light + dark. Visual regression snapshots for all four states.</li>
</ul>
</div>
</section>
</div>
</body>
</html>