Files
familienarchiv/docs/specs/dashboard-classic-split-final-spec.html
2026-04-14 23:21:15 +02:00

888 lines
56 KiB
HTML
Raw Permalink 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="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard — Classic Split · Final Design Spec · Familienarchiv</title>
<style>
*,*::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}
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
/* ── Masthead ─── */
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
.decisions{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
.dec-value s{color:rgba(255,255,255,.3);font-weight:400}
/* ── Section headings ─── */
.sec{margin-bottom:64px}
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
/* ── Screen grid ─── */
.sg{display:grid;gap:20px;align-items:start}
.sg-2{grid-template-columns:1fr 1fr}
.sg-mob{grid-template-columns:240px 1fr}
.sb{display:flex;flex-direction:column;gap:10px}
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
.sc{font-size:8.5px;color:#888;margin-top:6px;font-style:italic;line-height:1.5}
/* ── Annotation callouts ─── */
.ann{display:inline-block;font-size:7.5px;font-weight:700;color:#C2410C;background:#FFF7ED;border:1px solid #FDBA74;border-radius:3px;padding:1px 5px;white-space:nowrap}
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5;margin-top:10px}
.ann-block strong{font-weight:800}
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
/* ── Mock browser 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:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
.dot{width:7px;height:7px;border-radius:50%}
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
/* ── Nav bar ─── */
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
.N-accent{height:2px;background:#A6DAD8}
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
.nl.on{color:#fff}
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
.bell-dot{width:4px;height:4px;background:#A6DAD8;border-radius:50%;position:absolute;top:2px;right:2px}
/* ── Page body ─── */
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
/* ── Search bar ─── */
.SEARCH{display:flex;gap:6px;align-items:center}
.SEARCH-BOX{flex:1;height:28px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;display:flex;align-items:center;gap:6px;padding:0 9px}
.SEARCH-BOX input{border:none;outline:none;font-size:8px;color:#1a1a1a;flex:1;background:transparent}
.SEARCH-BOX input::placeholder{color:#C8C4BE}
.FILTER-BTN{height:28px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;padding:0 9px;font-size:7px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#555;display:flex;align-items:center;gap:4px}
/* ── Resume strip ─── */
.RESUME{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:5px 10px;font-size:7.5px;color:#555;display:flex;align-items:center;gap:5px}
.RESUME strong{color:#002850;font-weight:600}
/* ── Dashboard grid ─── */
/* No align-items → default is stretch → equal column heights */
.DASH-GRID{display:grid;grid-template-columns:1fr 200px;gap:8px}
/* ── Recent docs card ─── */
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 7px;border-bottom:1px solid #E0DDD5}
.CARD-HEAD h3{font-size:7px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
.CARD-HEAD a{font-size:7px;font-weight:600;color:#002850;opacity:.45;text-decoration:none}
.DOC-ROW{display:flex;align-items:baseline;justify-content:space-between;padding:5px 10px;border-bottom:1px solid #F0EDE6}
.DOC-ROW:last-of-type{border-bottom:none}
.DOC-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850}
.DOC-DATE{font-size:6.5px;color:#C8C4BE;white-space:nowrap;margin-left:6px;flex-shrink:0}
.CARD-FOOT{padding:6px 10px;border-top:1px solid #F0EDE6}
.CARD-FOOT-TEXT{font-size:6.5px;color:#C8C4BE}
/* ── Sidebar ─── */
/* height:100% fills the grid cell so right column matches left column height */
.SIDEBAR{display:flex;flex-direction:column;gap:8px;height:100%}
/* ── Upload zone ─── */
.UPLOAD{border:1.5px dashed rgba(166,218,216,.7);border-radius:3px;background:rgba(166,218,216,.06);padding:14px 10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-align:center;cursor:pointer}
.UPLOAD:hover{background:rgba(166,218,216,.12)}
.UPLOAD-LABEL{font-size:7px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#002850;opacity:.65}
.UPLOAD-HINT{font-size:6.5px;color:#C8C4BE;line-height:1.5}
/* ── Needs metadata card ─── */
/* flex:1 fills remaining sidebar height after the upload zone */
.META-CARD{background:#fff;border:1px solid #E0DDD5;border-top:2px solid #F39C12;border-radius:3px;overflow:hidden;flex:1;display:flex;flex-direction:column}
.META-CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #F0EDE6}
.META-CARD-HEAD h3{font-size:7px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
.META-PILL{background:#FFF3CD;color:#856404;font-size:7px;font-weight:800;padding:1px 6px;border-radius:10px}
.META-ROW{display:flex;align-items:center;padding:5px 10px;border-bottom:1px solid #F0EDE6}
.META-ROW:last-of-type{border-bottom:none}
.META-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;flex:1}
.META-CARD-FOOT{padding:5px 10px;border-top:1px solid #F0EDE6}
/* ── Mobile chrome ─── */
.WF-M{background:#fff;border:2px solid #B8B4AE;border-radius:14px;overflow:hidden;width:220px;box-shadow:0 4px 18px rgba(0,0,0,.08)}
.WF-M-STATUS{height:16px;background:#002850;display:flex;align-items:center;justify-content:space-between;padding:0 8px}
.WF-M-TIME{font-size:6.5px;color:#fff;font-weight:700}
.N-M{height:28px;background:#002850;display:flex;align-items:center;padding:0 10px;gap:8px}
.MAIN-M{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#F5F4EF}
.PH{height:6px;background:#E8E4DF;border-radius:2px}
.w80{width:80%}.w70{width:70%}.w60{width:60%}.w50{width:50%}.w40{width:40%}
/* ── Changes panel ─── */
.CHANGES{background:#fff;border:1.5px solid #E0DDD6;border-radius:8px;padding:20px 24px;margin-bottom:0}
.CHANGES h2{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #E8E4DF}
.CHANGES-GRID{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.C-COL h3{font-size:10px;font-weight:800;color:#444;margin-bottom:8px}
.C-COL ul{list-style:none;display:flex;flex-direction:column;gap:5px}
.C-COL ul li{font-size:11px;color:#555;padding-left:18px;position:relative;line-height:1.5}
.C-COL.add li::before{content:'✦';position:absolute;left:0;color:#002850;font-size:8px;top:2px}
.C-COL.remove li::before{content:'✗';position:absolute;left:0;color:#DC2626;top:1px}
.C-COL.keep li::before{content:'→';position:absolute;left:0;color:#888}
.C-COL li code{font-family:monospace;font-size:10px;background:#F5F5F5;padding:0 4px;border-radius:2px}
/* ── Edge cases ─── */
.EDGE{background:#FFFBF0;border:1px solid #F0D090;border-radius:6px;padding:11px 15px;margin-bottom:8px}
.EDGE-LABEL{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#92400E;margin-bottom:4px}
.EDGE-BODY{font-size:11px;color:#555;line-height:1.6}
.EDGE-BODY code{font-family:monospace;font-size:10px;background:rgba(0,0,0,.06);padding:0 4px;border-radius:2px}
/* ── AC list ─── */
.AC{counter-reset:ac;display:flex;flex-direction:column;gap:7px}
.AC-ITEM{display:flex;align-items:flex-start;gap:10px;background:#fff;border:1px solid #E0DDD5;border-radius:5px;padding:10px 14px;font-size:11px;color:#333;line-height:1.6}
.AC-ITEM::before{counter-increment:ac;content:counter(ac);display:flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;border-radius:50%;background:#002850;color:#fff;font-size:9px;font-weight:900;margin-top:1px}
.AC-ITEM code{font-family:monospace;font-size:10px;background:#F5F5F5;padding:0 4px;border-radius:2px}
.AC-ITEM .tag{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;padding:1px 5px;border-radius:3px;margin-left:6px}
.AC-ITEM .tag-a11y{background:#EDE9FE;color:#5B21B6}
.AC-ITEM .tag-mobile{background:#DCFCE7;color:#166534}
.AC-ITEM .tag-data{background:#DBEAFE;color:#1E40AF}
/* ── Spec disclaimer ─── */
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
.spec-disclaimer strong{font-weight:800}
/* ── Agent Implementation Reference ─── */
.impl-ref{background:#0d1117;border-radius:8px;margin-top:20px;overflow:hidden;border:1px solid #30363d}
.impl-ref-hdr{background:#161b22;padding:9px 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:8px 14px;border-bottom:1px solid #21262d}
.impl-ref td{padding:6px 14px;border-bottom:1px solid #161b22;vertical-align:top;line-height:1.6;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:200px}
.impl-ref td code{font-family:'SFMono-Regular',Consolas,monospace;font-size:9.5px;background:#161b22;color:#a5d6ff;padding:1px 5px;border-radius:3px;white-space:nowrap}
.impl-ref .ir-px{color:#7ee787;font-family:monospace;font-size:9.5px}
</style>
</head>
<body>
<div class="doc">
<!-- ══════════════════════════════════
MASTHEAD
══════════════════════════════════ -->
<div class="mast">
<div class="mast-top">
<div>
<h1>Dashboard — Classic Split · Final Design Spec</h1>
<p>Refocus the homepage on documents. The notification widget is removed from the dashboard — it already lives in the bell dropdown. The page is restructured into a two-column "Command Center": recent activity on the left, upload zone and missing-metadata queue on the right. Stats are demoted to a quiet footnote.</p>
</div>
<span class="mast-badge">Final · Ready for implementation</span>
</div>
<div class="decisions">
<div class="dec">
<div class="dec-label">Notification widget</div>
<div class="dec-value"><s>On dashboard</s><br>→ Bell dropdown only</div>
</div>
<div class="dec">
<div class="dec-label">Layout</div>
<div class="dec-value"><s>Single column stacked</s><br>→ 2-col split (desktop)</div>
</div>
<div class="dec">
<div class="dec-label">Upload button in action bar</div>
<div class="dec-value"><s>Redundant button</s><br>→ Upload zone only</div>
</div>
<div class="dec">
<div class="dec-label">Stats</div>
<div class="dec-value"><s>Prominent stat chips</s><br>→ Quiet footnote text</div>
</div>
<div class="dec">
<div class="dec-label">Backend changes</div>
<div class="dec-value">None — <code style="color:#A6DAD8;font-size:9px">/api/stats</code> already exists</div>
</div>
</div>
</div>
<!-- spec disclaimer -->
<div class="spec-disclaimer">
<strong>📐 Mockup scale notice —</strong> all font-size, height, and padding values in the mockup CSS below are scaled to ~55% of actual implementation values.
<strong>Do not copy sizes from mockup CSS.</strong> Use the ⚙ Implementation Reference tables after each section.
</div>
<!-- ══════════════════════════════════
SECTION 1 — DESKTOP LAYOUT
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">1</span> Desktop Layout — ≥ 1024 px</div>
<div class="sg sg-2" style="align-items:start">
<div class="sb">
<div class="sl">Full page <span class="sz">1440px</span></div>
<!-- Mock browser -->
<div class="wf">
<div class="wf-bar">
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
<div class="urlbar"><span>familienarchiv.local /</span></div>
</div>
<!-- Nav -->
<div class="N">
<span class="logo">Familienarchiv</span>
<span class="nl on">Documents</span>
<span class="nl">Persons</span>
<span class="nl">Correspondence</span>
<div class="nr">
<!-- settings icon -->
<div class="nico"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09"/></svg></div>
<!-- bell with dot -->
<div class="nico" style="position:relative">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
<div style="position:absolute;top:2px;right:2px;width:4px;height:4px;background:#A6DAD8;border-radius:50%;"></div>
</div>
<div class="av">BC</div>
</div>
</div>
<div class="N-accent"></div>
<!-- Page body -->
<div class="MAIN">
<!-- Search -->
<div class="SEARCH">
<div class="SEARCH-BOX">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" placeholder="Search in title, content, location…" disabled>
</div>
<button class="FILTER-BTN">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="4" y1="6" x2="20" y2="6"/><line x1="8" y1="12" x2="16" y2="12"/><line x1="11" y1="18" x2="13" y2="18"/></svg>
Filter
</button>
</div>
<!-- Resume strip -->
<div class="RESUME">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4z"/></svg>
Continue where you left off: <strong>E28 History Tee Dokument (bearbeitet)</strong>
</div>
<!-- 2-col dashboard grid -->
<div class="DASH-GRID">
<!-- LEFT: Recent docs -->
<div class="CARD">
<div class="CARD-HEAD">
<h3>Recent Activity</h3>
<a href="#">All documents →</a>
</div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — review</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — version</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="CARD-FOOT">
<span class="CARD-FOOT-TEXT">248 Documents &nbsp;·&nbsp; 34 Persons</span>
</div>
</div>
<!-- RIGHT: Sidebar -->
<div class="SIDEBAR">
<!-- Upload zone -->
<div class="UPLOAD">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
<div class="UPLOAD-LABEL">Drop files here</div>
<div class="UPLOAD-HINT">PDF, PNG, JPG, ODS, XLS<br>or click to browse</div>
</div>
<!-- Needs metadata -->
<div class="META-CARD">
<div class="META-CARD-HEAD">
<h3>Needs Metadata</h3>
<span class="META-PILL">5</span>
</div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-CARD-FOOT"><a href="#" style="font-size:6.5px;font-weight:600;color:#002850;opacity:.45;text-decoration:none">Show all →</a></div>
</div>
</div>
</div><!-- /DASH-GRID -->
</div><!-- /MAIN -->
</div><!-- /wf -->
<div class="ann-block">
<strong>Key decisions visible here</strong>
<ul>
<li>Notification widget removed entirely — bell badge in header is sufficient</li>
<li>Upload zone replaces the action-bar button — no redundancy</li>
<li>Stats footnote: <em>quiet</em>, not a chip — does not compete for attention</li>
<li>Right column is 300 px fixed — enough for upload + short metadata list</li>
</ul>
</div>
</div>
<!-- Annotated callouts panel -->
<div class="sb">
<div class="sl">Annotations</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;color:#333;line-height:1.7;display:flex;flex-direction:column;gap:14px;">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">① Search bar</div>
Unchanged — existing <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">SearchFilterBar</code> component. Full width, no upload button appended.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">② Resume strip</div>
Unchanged — <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardResumeStrip</code>. Only renders when localStorage has a last-visited document. Hidden otherwise — no empty gap.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">③ Dashboard grid</div>
<code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4</code><br>
No <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">items-start</code> — the CSS Grid default <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">align-items: stretch</code> makes both columns the same height for free. The right column wrapper needs <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">h-full</code> so the flex container fills that height, and the metadata card gets <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">flex-1</code> to consume the space left after the upload zone. Both columns are always flush at the bottom.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">④ Recent Activity card</div>
<code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardRecentDocuments</code> receives a new optional <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats</code> prop. The footnote only renders when <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats?.totalDocuments != null</code>.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">⑤ Upload zone</div>
Existing <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DropZone</code> component, no internal changes. Wrapped in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">{#if data.canWrite}</code> — hidden for read-only users.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">⑥ Needs Metadata card</div>
Unchanged <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardNeedsMetadata</code>. Already renders nothing when <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">incompleteDocs.length === 0</code>. Amber top border signals "action required" without relying on color alone — the heading "Needs Metadata" and count pill are redundant cues.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:4px;">⑦ Right column empty state</div>
If both upload zone (no canWrite) and needs-metadata (no incomplete docs) are absent, the right grid cell is an empty <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">&lt;div&gt;</code>. The equal-height stretch still applies but an invisible column causes no visual artefact. When this case is detectable server-side, consider conditionally omitting the grid class so the left column runs full width — but this is a polish improvement, not a blocker.
</div>
</div>
</div>
</div>
<!-- impl-ref: Desktop layout -->
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Desktop Layout
<span>Real values · mockup above is ~55% scale · do not copy mockup CSS</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Page wrapper</td>
<td><code>mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8</code></td>
<td><span class="ir-px">py 32px, max-w 1280px</span></td>
<td>Unchanged from current <code>+page.svelte</code></td>
</tr>
<tr>
<td>Resume strip</td>
<td><code>mb-4</code> (existing component, no change)</td>
<td><span class="ir-px">mb 16px</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Dashboard grid wrapper</td>
<td><code>mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px]</code></td>
<td><span class="ir-px">gap 16px, right col 300px fixed</span></td>
<td><strong>No <code>items-start</code>.</strong> CSS Grid default is <code>align-items: stretch</code> — both columns are automatically the same height. Replaces the current conditional mentions+metadata grid.</td>
</tr>
<tr>
<td>Right column inner wrapper</td>
<td><code>flex flex-col gap-4 h-full</code></td>
<td><span class="ir-px">gap 16px, full grid cell height</span></td>
<td><code>h-full</code> is required so the flex container fills the stretched grid cell. Without it the column stops at content height and <code>flex-1</code> on the child has nothing to fill.</td>
</tr>
<tr>
<td>DashboardNeedsMetadata wrapper</td>
<td><code>flex-1 flex flex-col min-h-0</code> (wraps the component)</td>
<td><span class="ir-px">grows to fill remaining height</span></td>
<td><code>flex-1</code> consumes the space left after the DropZone. <code>min-h-0</code> prevents flex overflow. The component's inner card should be <code>h-full</code> so the card border fills the space — content sits at the top, not stretched.</td>
</tr>
<tr>
<td>DropZone guard</td>
<td><code>{#if data.canWrite}</code></td>
<td></td>
<td>No changes to the DropZone component itself — wrapper condition only. When absent, the metadata card's <code>flex-1</code> still fills the full right column height.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 2 — MOBILE LAYOUT
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">2</span> Mobile Layout — &lt; 1024 px · stacking order</div>
<div class="sg sg-mob" style="align-items:start;gap:32px">
<!-- Mobile mockup -->
<div class="sb">
<div class="sl">375 px <span class="sz">iPhone</span></div>
<div class="WF-M">
<div class="WF-M-STATUS">
<span class="WF-M-TIME">09:41</span>
<div style="display:flex;gap:3px"><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div></div>
</div>
<div class="N-M">
<span class="logo" style="font-size:7px">Familienarchiv</span>
<div class="nr">
<div class="nico" style="position:relative">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
<div style="position:absolute;top:2px;right:2px;width:3px;height:3px;background:#A6DAD8;border-radius:50%;"></div>
</div>
<div class="av" style="width:16px;height:16px;font-size:5px">BC</div>
</div>
</div>
<div class="N-accent"></div>
<div class="MAIN-M">
<!-- search -->
<div style="height:22px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;display:flex;align-items:center;padding:0 8px;gap:5px">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<div class="PH w70" style="height:5px"></div>
</div>
<!-- resume -->
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:4px 8px;display:flex;align-items:center;gap:4px">
<div class="PH w60" style="height:5px;background:#E8E4DF"></div>
</div>
<!-- ① recent docs -->
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden">
<div style="padding:5px 8px;border-bottom:1px solid #F0EDE6;display:flex;justify-content:space-between">
<div class="PH w40" style="height:5px"></div>
<div class="PH w20" style="height:5px;width:20%"></div>
</div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w80" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w80" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w60" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:5px 8px;border-top:1px solid #F0EDE6"><div class="PH w40" style="height:4px"></div></div>
</div>
<!-- ② upload -->
<div style="border:1.5px dashed rgba(166,218,216,.7);border-radius:3px;background:rgba(166,218,216,.06);padding:12px 8px;display:flex;flex-direction:column;align-items:center;gap:3px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
<div class="PH w50" style="height:5px"></div>
<div class="PH w70" style="height:4px"></div>
</div>
<!-- ③ needs metadata -->
<div style="background:#fff;border:1px solid #E0DDD5;border-top:2px solid #F39C12;border-radius:3px;overflow:hidden">
<div style="padding:5px 8px;border-bottom:1px solid #F0EDE6;display:flex;justify-content:space-between;align-items:center">
<div class="PH w40" style="height:5px"></div>
<div style="background:#FFF3CD;border-radius:8px;padding:1px 5px;font-size:6px;font-weight:800;color:#856404">5</div>
</div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w60" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
</div>
</div>
</div>
<div class="sc">Stacking order on mobile: recent docs → upload → metadata</div>
</div>
<!-- Explanation -->
<div class="sb">
<div class="sl">Stacking order rationale</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;display:flex;flex-direction:column;gap:14px;color:#333">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">① Recent Activity — first</div>
The most common task on mobile: browsing recently-touched documents. This should be immediately visible without scrolling past an upload zone most users won't use every visit.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">② Upload zone — second</div>
Mobile uploads happen but are less frequent than browsing. Positioned after the list so it doesn't block the primary use case, but still reachable with a single scroll.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">③ Needs Metadata — last</div>
Metadata enrichment on mobile is uncommon (small screen, lots of form fields). It appears last — accessible to those who need it, invisible noise to everyone else.
</div>
<div style="background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:10px 12px;font-size:11px;color:#14532D">
<strong>Touch targets:</strong> All interactive rows in DashboardRecentDocuments and DashboardNeedsMetadata must meet <strong>min-height 44px</strong> (WCAG 2.5.5). The upload zone's click target is the full box — no small button.
</div>
</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;margin-top:0">
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:10px">Dual-audience notes</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div>
<span style="font-size:8px;font-weight:800;background:#002850;color:#A6DAD8;padding:1px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px">Seniors 60+</span>
<div style="margin-top:4px">Document title is Merriweather serif at <strong>18px minimum</strong> — the most commonly undersized element in this type of list. Date label in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">text-ink-3</code> at 12px — acceptable for supplementary metadata but never below that. Sufficient line-height (1.6) and border separators provide clear row breaks without relying on color.</div>
</div>
<div>
<span style="font-size:8px;font-weight:800;background:#374151;color:#D1FAE5;padding:1px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px">Millennials</span>
<div style="margin-top:4px">Information density is preserved on desktop. The upload zone accepts drag-and-drop natively — no button required for the gesture-native user. Stats footnote satisfies curiosity without cluttering the primary view.</div>
</div>
</div>
</div>
</div>
</div>
<!-- impl-ref: Mobile -->
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Mobile Stacking
<span>Real values · mockup above is ~55% scale</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Grid (mobile)</td>
<td><code>grid-cols-1</code> (default, overridden at lg)</td>
<td><span class="ir-px">full width</span></td>
<td>No explicit mobile grid — single column is the default</td>
</tr>
<tr>
<td>Doc row touch target</td>
<td><code>flex items-center justify-between py-3 border-b border-line</code></td>
<td><span class="ir-px">min-h 44px via py-3 + content</span></td>
<td><strong>Most commonly undersized.</strong> py-3 (12px × 2) + 18px text = ~42px. Add <code>min-h-[44px]</code> to guarantee WCAG 2.5.5</td>
</tr>
<tr>
<td>Metadata row touch target</td>
<td><code>flex items-center border-b border-line py-3</code></td>
<td><span class="ir-px">min-h 44px</span></td>
<td>Same rule — <code>min-h-[44px]</code> required</td>
</tr>
<tr>
<td>Upload zone (mobile)</td>
<td>Existing DropZone — no change to component</td>
<td><span class="ir-px">full width, py-6</span></td>
<td>Entire zone is the click target — WCAG 2.5.5 satisfied by size</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 3 — RECENT ACTIVITY CARD + STATS FOOTNOTE
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">3</span> Recent Activity Card — stats footnote detail</div>
<div class="sg sg-2" style="align-items:start">
<div class="sb">
<div class="sl">Component detail</div>
<!-- Card mockup -->
<div class="CARD" style="max-width:460px">
<div class="CARD-HEAD">
<h3>Recent Activity</h3>
<a href="#">All documents →</a>
</div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — review</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — version</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="CARD-FOOT" style="display:flex;align-items:center;justify-content:space-between">
<span class="CARD-FOOT-TEXT">248 Documents &nbsp;·&nbsp; 34 Persons</span>
<!-- middle dot acts as separator, not a color-only cue -->
</div>
</div>
<div class="ann-block">
<strong>Stats footnote rules</strong>
<ul>
<li>Only renders when <code>stats?.totalDocuments != null</code></li>
<li>Persons count follows only when <code>stats?.totalPersons != null</code></li>
<li>The middle dot <code>·</code> is a text separator — not a visual-only cue</li>
<li>Uses <code>text-ink-3</code> token — light enough to recede, but still WCAG AA (4.5:1) on white surface</li>
<li>No units abbreviation: "248 Documents", not "248 docs" — plain language for seniors</li>
</ul>
</div>
</div>
<div class="sb">
<div class="sl">Component prop change</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;display:flex;flex-direction:column;gap:12px">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">DashboardRecentDocuments.svelte — new prop</div>
<pre style="background:#0d1117;color:#a5d6ff;font-family:monospace;font-size:10px;padding:12px 14px;border-radius:5px;line-height:1.8;overflow-x:auto">interface Props {
recentDocs: Document[];
stats?: {
totalDocuments?: number;
totalPersons?: number;
} | null;
}</pre>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">Footnote template snippet</div>
<pre style="background:#0d1117;color:#a5d6ff;font-family:monospace;font-size:10px;padding:12px 14px;border-radius:5px;line-height:1.8;overflow-x:auto">{#if stats?.totalDocuments != null}
&lt;div class="mt-2 border-t border-line
pt-3 font-sans text-xs
text-ink-3"&gt;
{stats.totalDocuments}
{m.dashboard_stats_documents()}
{#if stats.totalPersons != null}
&nbsp;·&nbsp;
{stats.totalPersons}
{m.dashboard_stats_persons()}
{/if}
&lt;/div&gt;
{/if}</pre>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">New i18n keys (all 3 locales)</div>
<table style="width:100%;font-size:10px;border-collapse:collapse">
<thead><tr style="border-bottom:1.5px solid #E0DDD5"><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">Key</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">de</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">en</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">es</th></tr></thead>
<tbody>
<tr style="border-bottom:1px solid #F0EDE6"><td style="padding:4px 6px;font-family:monospace;font-size:9px">dashboard_stats_documents</td><td style="padding:4px 6px">Dokumente</td><td style="padding:4px 6px">Documents</td><td style="padding:4px 6px">Documentos</td></tr>
<tr><td style="padding:4px 6px;font-family:monospace;font-size:9px">dashboard_stats_persons</td><td style="padding:4px 6px">Personen</td><td style="padding:4px 6px">Persons</td><td style="padding:4px 6px">Personas</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Recent Activity Card
<span>Real values · mockup above is ~55% scale</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Card container</td>
<td><code>rounded-sm border border-line bg-surface</code></td>
<td><span class="ir-px">border 1px</span></td>
<td>Unchanged from existing DashboardRecentDocuments styles</td>
</tr>
<tr>
<td>Card heading row</td>
<td><code>flex items-center justify-between px-4 pt-4 pb-3</code></td>
<td><span class="ir-px">px 16px, pt 16px, pb 12px</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Section heading text</td>
<td><code>font-sans text-xs font-bold tracking-widest text-gray-400 uppercase</code></td>
<td><span class="ir-px">12px / 700</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Document title</td>
<td><code>font-serif text-lg text-ink hover:text-ink-2 hover:underline</code></td>
<td><span class="ir-px">18px / 400 — most commonly undersized</span></td>
<td><strong>Must not fall below 18px.</strong> Serves both readability (seniors) and visual hierarchy</td>
</tr>
<tr>
<td>Date label</td>
<td><code>ml-2 shrink-0 font-sans text-xs text-gray-400</code></td>
<td><span class="ir-px">12px</span></td>
<td>Minimum permitted size for supplementary metadata — do not reduce further</td>
</tr>
<tr>
<td>Document row</td>
<td><code>flex items-center justify-between border-b border-line py-2 px-4 last:border-0</code></td>
<td><span class="ir-px">py 8px, min-h ~44px with 18px text</span></td>
<td>Add <code>min-h-[44px]</code> to guarantee WCAG 2.5.5 touch target</td>
</tr>
<tr>
<td>Stats footnote wrapper</td>
<td><code>mt-2 border-t border-line px-4 pt-3 pb-4 font-sans text-xs text-ink-3</code></td>
<td><span class="ir-px">12px / 400, pt 12px, pb 16px</span></td>
<td>New addition. <code>text-ink-3</code> token must pass 4.5:1 on <code>bg-surface</code> — verify in both light and dark mode</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 4 — SERVER DATA CHANGES
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">4</span> Server Data Changes — +page.server.ts</div>
<div class="sg sg-2" style="align-items:start">
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;display:flex;flex-direction:column;gap:14px">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#DC2626;margin-bottom:6px">Remove</div>
<ul style="padding-left:16px;display:flex;flex-direction:column;gap:4px">
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">/api/notifications</code> fetch from <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">Promise.allSettled</code> — the bell component fetches its own data client-side</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">mentions: NotificationDTO[]</code> variable and its allSettled result handling</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">mentions</code> key from the return object</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">NotificationDTO</code> type import (no longer used in this file)</li>
</ul>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:6px">Add</div>
<ul style="padding-left:16px;display:flex;flex-direction:column;gap:4px">
<li>A <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">/api/stats</code> GET call inside the <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">isDashboard</code> allSettled block</li>
<li><code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats: StatsDTO | null</code> in the return — <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">null</code> on any failure</li>
<li><code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">StatsDTO</code> import from generated types (already in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">src/lib/generated/api.ts</code>)</li>
</ul>
</div>
</div>
<div style="background:#0d1117;border-radius:6px;overflow:hidden;border:1px solid #30363d">
<div style="background:#161b22;padding:8px 14px;font-size:9px;font-weight:800;color:#f0883e;border-bottom:1px solid #30363d;text-transform:uppercase;letter-spacing:.4px">allSettled block — after change</div>
<pre style="color:#a5d6ff;font-family:monospace;font-size:10px;padding:14px 16px;line-height:1.8;overflow-x:auto">const [incompleteResult,
recentResult,
statsResult] =
await Promise.allSettled([
api.GET('/api/documents/incomplete',
{ params: { query: { size: 5 } } }),
api.GET('/api/documents/recent-activity',
{ params: { query: { size: 5 } } }),
api.GET('/api/stats'),
]);
<span style="color:#8b949e">// … existing incomplete/recent handling …</span>
let stats: StatsDTO | null = null;
if (statsResult.status === 'fulfilled'
&& statsResult.value.response.ok) {
stats = statsResult.value.data ?? null;
}</pre>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 5 — CHANGES SUMMARY
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">5</span> Changes Summary</div>
<div class="CHANGES">
<h2>All files touched</h2>
<div class="CHANGES-GRID">
<div class="C-COL add">
<h3>Added / New behaviour</h3>
<ul>
<li><code>mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px]</code> grid in +page.svelte</li>
<li><code>stats</code> prop on DashboardRecentDocuments</li>
<li>Stats footnote inside DashboardRecentDocuments</li>
<li><code>/api/stats</code> fetch in +page.server.ts</li>
<li><code>dashboard_stats_documents</code> i18n key (de / en / es)</li>
<li><code>dashboard_stats_persons</code> i18n key (de / en / es)</li>
</ul>
</div>
<div class="C-COL remove">
<h3>Removed</h3>
<ul>
<li><code>DashboardMentions</code> import and usage in +page.svelte</li>
<li><code>/api/notifications</code> fetch from server load</li>
<li><code>mentions</code> variable and return value in server load</li>
<li><code>NotificationDTO</code> import in +page.server.ts</li>
<li>The conditional 2-col grid for mentions+metadata</li>
</ul>
</div>
<div class="C-COL keep">
<h3>Kept unchanged</h3>
<ul>
<li><code>DashboardMentions.svelte</code> file (not deleted)</li>
<li><code>DashboardNeedsMetadata.svelte</code> (no changes)</li>
<li><code>DashboardResumeStrip.svelte</code> (no changes)</li>
<li><code>DropZone.svelte</code> (no changes)</li>
<li><code>NotificationBell.svelte</code> — already has "View all" link</li>
<li><code>SearchFilterBar.svelte</code> (no changes)</li>
</ul>
</div>
<div class="C-COL keep">
<h3>Explicitly out of scope</h3>
<ul>
<li>Dedicated <code>/notifications</code> overview page</li>
<li>DropZone accepted file types or upload behaviour</li>
<li>Dark mode token adjustments</li>
<li>Backend changes (none needed)</li>
<li>Any changes to admin, persons, or correspondence routes</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 6 — EDGE CASES
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">6</span> Edge Cases</div>
<div class="sg sg-2" style="align-items:start">
<div>
<div class="EDGE">
<div class="EDGE-LABEL">Read-only user (no canWrite)</div>
<div class="EDGE-BODY">DropZone is hidden. Right column contains only DashboardNeedsMetadata. If there are also no incomplete documents, the right <code>&lt;div class="flex flex-col gap-4"&gt;</code> is empty — the grid column produces no visual gap, recent activity expands naturally.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No incomplete documents</div>
<div class="EDGE-BODY">DashboardNeedsMetadata renders nothing (already guarded by <code>incompleteDocs.length > 0</code>). Combined with a canWrite user, the right column shows only the upload zone.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No recent documents (new / empty archive)</div>
<div class="EDGE-BODY">DashboardRecentDocuments already handles empty state (renders nothing when <code>recentDocs.length === 0</code>). Stats footnote still renders as long as the API call succeeded — "0 Documents · 0 Persons" is valid and informative.</div>
</div>
</div>
<div>
<div class="EDGE">
<div class="EDGE-LABEL">/api/stats fetch fails</div>
<div class="EDGE-BODY"><code>Promise.allSettled</code> isolates the failure. <code>stats</code> is returned as <code>null</code>. The <code>{#if stats?.totalDocuments != null}</code> guard silently suppresses the footnote. Everything else renders normally — no error banner, no visual regression.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No last-visited document in localStorage</div>
<div class="EDGE-BODY">DashboardResumeStrip already handles this — it renders nothing. No gap between search bar and the dashboard grid.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">Very long document title in recent activity</div>
<div class="EDGE-BODY">Title should be truncated with <code>truncate</code> Tailwind class (already present in existing component — verify). The date label has <code>shrink-0</code> so it is never squeezed off-screen.</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 7 — ACCEPTANCE CRITERIA
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">7</span> Acceptance Criteria</div>
<div class="AC">
<div class="AC-ITEM">Dashboard page no longer renders the notifications/mentions widget. The bell icon in the header continues to work and its dropdown still shows the "View all notifications" link.</div>
<div class="AC-ITEM">On viewports ≥ 1024 px the dashboard shows a two-column grid: recent activity left (~remaining width), sidebar right (300 px fixed). <span class="tag tag-mobile">mobile</span></div>
<div class="AC-ITEM">On viewports &lt; 1024 px the columns stack: recent docs first, upload zone second, needs-metadata third. <span class="tag tag-mobile">mobile</span></div>
<div class="AC-ITEM">All interactive document rows have a minimum touch target height of 44 px. <span class="tag tag-a11y">WCAG 2.5.5</span></div>
<div class="AC-ITEM">Document titles in the recent-activity list render at minimum 18 px (Merriweather serif, <code>text-lg</code>). <span class="tag tag-a11y">WCAG 1.4.4</span></div>
<div class="AC-ITEM">Stats footnote "248 Documents · 34 Persons" appears at the bottom of the recent-activity card in <code>text-xs text-ink-3</code>. It is absent when the <code>/api/stats</code> call fails or returns null. <span class="tag tag-data">data</span></div>
<div class="AC-ITEM">Read-only users (no canWrite permission) do not see the upload zone. The dashboard still renders correctly without it.</div>
<div class="AC-ITEM">When no incomplete documents exist, the Needs Metadata card is absent. The right column shows only the upload zone (or is empty for read-only users) — no visual gap or empty box.</div>
<div class="AC-ITEM"><code>npm run check</code> passes — no TypeScript errors. The new <code>stats</code> prop on DashboardRecentDocuments is typed as <code>StatsDTO | null | undefined</code>.</div>
<div class="AC-ITEM"><code>npm run lint</code> passes — no Prettier or ESLint errors.</div>
</div>
</div>
</div>
</body>
</html>