Files
familienarchiv/docs/specs/conversations-narrow-column.html
Marcel 9f73c2ee4a
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
docs: add conversations page Narrow Column redesign spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 18:28:22 +02:00

1253 lines
83 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="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conversations — Narrow Column Redesign 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:1560px;margin:0 auto;padding:48px 32px}
/* Masthead */
.masthead{background:#0D2240;border-radius:10px;padding:36px 40px;margin-bottom:56px;display:flex;align-items:flex-start;justify-content:space-between;gap:24px}
.masthead-left h1{font-size:26px;font-weight:900;color:#fff;letter-spacing:-.5px;margin-bottom:6px}
.masthead-left p{font-size:13px;color:rgba(255,255,255,.55);max-width:600px;line-height:1.6}
.masthead-meta{margin-top:14px;display:flex;gap:10px;flex-wrap:wrap}
.masthead-tag{font-size:10px;font-weight:800;padding:3px 10px;border-radius:20px;text-transform:uppercase;letter-spacing:1px}
.v-badge{background:#A6DAD8;color:#002850}
.file-badge{background:rgba(255,255,255,.12);color:rgba(255,255,255,.7)}
.issue-badge{background:rgba(166,218,216,.15);color:#A6DAD8;border:1px solid rgba(166,218,216,.3)}
/* Section layout */
.section{margin-bottom:72px}
.section+.section{border-top:2px dashed #C8C4BE;padding-top:64px}
.sec-intro{display:flex;align-items:flex-start;gap:20px;margin-bottom:28px}
.sec-num{font-size:64px;font-weight:900;color:#DDD;line-height:1;flex-shrink:0;width:80px}
.sec-badge{display:inline-block;font-size:10px;font-weight:800;padding:3px 10px;border-radius:20px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px}
.badge-arch{background:#F0F0F0;color:#555}
.badge-filter{background:#E3EEFF;color:#1D4ED8}
.badge-chat{background:#D6F0EE;color:#0F5B58}
.badge-bubble{background:#FEF3C7;color:#B45309}
.badge-divider{background:#F3E8FF;color:#7C3AED}
.badge-summary{background:#DCFCE7;color:#166534}
.badge-empty{background:#FEE2E2;color:#B91C1C}
.badge-impl{background:#F0F0F0;color:#444}
.sec-title{font-size:20px;font-weight:800;color:#002850;margin-bottom:4px}
.sec-tagline{font-size:13px;color:#666;max-width:680px;line-height:1.6}
/* Wireframe frames */
.screens{display:flex;gap:20px;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap}
.screens.cols2{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.screens.cols3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
.screen-block{display:flex;flex-direction:column;min-width:0}
.screen-label{font-size:10px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px;display:flex;align-items:center;gap:8px}
.sz{background:#E8E4DF;color:#666;padding:1px 6px;border-radius:3px;font-size:9px}
.state-tag{padding:1px 7px;border-radius:3px;font-size:9px;font-weight:700;background:#DBEAFE;color:#1E40AF}
.state-tag.active{background:#DCFCE7;color:#166534}
.state-tag.collapsed{background:#FEF3C7;color:#92400E}
.state-tag.mobile{background:#F3E8FF;color:#7C3AED}
.screen-cap{font-size:9px;color:#888;margin-top:6px;text-align:center;font-style:italic;line-height:1.5}
/* Wireframe shell */
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.08);position:relative}
.wf-bar{height:26px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 10px;gap:5px}
.dot{width:8px;height:8px;border-radius:50%;background:#C8C4BE}
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
.urlbar{flex:1;height:13px;background:#D8D4CE;border-radius:3px;margin-left:8px;display:flex;align-items:center;padding:0 6px}
.urlbar span{font-size:8px;color:#888;font-family:monospace}
/* Navbar */
.N{height:44px;background:#0D2240;display:flex;align-items:center;padding:0 18px;gap:14px;flex-shrink:0}
.N-m{height:46px;background:#0D2240;display:flex;align-items:center;padding:0 14px;justify-content:space-between;flex-shrink:0}
.logo{font-size:10px;font-weight:900;color:#fff;letter-spacing:1px}
.nl{font-size:8px;color:rgba(255,255,255,.5);font-weight:700;text-transform:uppercase;letter-spacing:.5px}
.nl.on{color:#fff;border-bottom:2px solid #A6DAD8;padding-bottom:2px}
.nr{margin-left:auto;display:flex;gap:8px;align-items:center}
.nav-ico{width:22px;height:22px;background:rgba(255,255,255,.12);border-radius:5px}
.nav-user{font-size:8px;color:rgba(255,255,255,.6);font-weight:700}
/* Main content areas */
.MAIN{flex:1;padding:16px 20px;display:flex;flex-direction:column;gap:10px;min-width:0;background:#ECEAE4}
.MAIN-sm{flex:1;padding:10px 14px;display:flex;flex-direction:column;gap:8px;min-width:0;background:#ECEAE4}
/* Page chrome */
.PAGE-HEAD{border-bottom:1px solid rgba(26,26,26,.1);padding-bottom:8px;margin-bottom:10px}
.PAGE-H1{font-size:14px;font-weight:800;color:#0D2240;font-family:Georgia,serif}
.PAGE-SUB{font-size:8px;color:#888;margin-top:2px}
/* Filter bar variants */
.FB-FULL{border:1.5px solid #C8C4BE;background:#F7F5F2;padding:14px;box-shadow:0 2px 8px rgba(0,0,0,.04);margin-bottom:10px}
.FB-STRIP{border-bottom:1.5px solid #E0DDD6;background:#F7F5F2;padding:7px 12px;display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.FB-STRIP-left{display:flex;align-items:center;gap:8px}
.FB-STRIP-names{font-size:10px;font-weight:700;color:#0D2240;font-family:Georgia,serif}
.FB-STRIP-meta{font-size:8px;color:#999}
.FB-STRIP-btn{font-size:8px;font-weight:800;color:#002850;border:1.5px solid #002850;padding:3px 8px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;white-space:nowrap}
.FB-ROW1{display:grid;grid-template-columns:1fr 28px 1fr;gap:8px;align-items:end;margin-bottom:10px}
.FB-ROW2{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}
.FB-LABEL{font-size:7.5px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px}
.FB-INPUT{height:28px;border:1.5px solid #D1D5DB;border-radius:3px;background:#fff;display:flex;align-items:center;padding:0 8px;font-size:9px;color:#333}
.FB-INPUT.placeholder{color:#AAA;font-style:italic}
.FB-SWAP{width:28px;height:28px;border:1.5px solid #C8C4BE;border-radius:3px;background:#F0EDE8;display:flex;align-items:center;justify-content:center;font-size:10px;color:#666;align-self:flex-end}
.FB-SORT{height:28px;border:1.5px solid #D1D5DB;border-radius:3px;background:#fff;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:#444;gap:3px;text-transform:uppercase;letter-spacing:.3px}
.FB-APPLY{height:28px;background:#002850;color:#fff;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;margin-top:10px}
.FB-CANCEL{font-size:8px;color:#888;text-decoration:underline;text-align:center;margin-top:5px;cursor:pointer}
/* Overlay panel */
.FB-OVERLAY{border:1.5px solid #002850;background:#fff;padding:14px;box-shadow:0 8px 24px rgba(0,0,0,.12);margin-bottom:10px;position:relative}
.FB-OVERLAY::before{content:'▲';position:absolute;top:-9px;left:20px;color:#002850;font-size:10px}
/* Chat column */
.CHAT-OUTER{border:1.5px solid #E0DDD6;background:#F7F5F2;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.05);padding:12px;position:relative;overflow:hidden}
.CHAT-COL{max-width:300px;margin:0 auto;display:flex;flex-direction:column;gap:6px}
.CHAT-COL-FULL{width:100%;display:flex;flex-direction:column;gap:6px}
/* Bubble rows */
.BUBBLE-ROW{display:flex;width:100%}
.BUBBLE-ROW.right{justify-content:flex-end}
.BUBBLE-ROW.left{justify-content:flex-start}
.BUBBLE-GROUP{display:flex;gap:5px;max-width:75%}
.BUBBLE-GROUP.right{flex-direction:row-reverse}
.BUBBLE-GROUP.left{flex-direction:row}
.AVATAR{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:6px;font-weight:800;flex-shrink:0;align-self:flex-end;margin-bottom:2px}
.AVATAR.sender{background:#002850;color:#fff;border:1px solid #002850}
.AVATAR.receiver{background:#fff;color:#444;border:1.5px solid #C8C4BE}
.BUBBLE{border-radius:6px;padding:7px 9px;max-width:100%}
.BUBBLE.sender{background:#002850;color:#fff;border-bottom-right-radius:2px;border:1px solid #002850}
.BUBBLE.receiver{background:#E8E4DF;color:#1A1A1A;border-bottom-left-radius:2px;border:1.5px solid #D1CCC8}
.BUBBLE-TITLE{font-size:8px;font-weight:700;font-family:Georgia,serif;line-height:1.3;margin-bottom:3px}
.BUBBLE.sender .BUBBLE-TITLE{color:#fff}
.BUBBLE.receiver .BUBBLE-TITLE{color:#1A1A1A}
.BUBBLE-META{font-size:7px;text-transform:uppercase;letter-spacing:.4px;opacity:.7;margin-bottom:3px}
.BUBBLE.sender .BUBBLE-META{color:rgba(255,255,255,.7)}
.BUBBLE.receiver .BUBBLE-META{color:#666}
.BUBBLE-STATUS{display:flex;align-items:center;gap:3px}
.STATUS-DOT{width:5px;height:5px;border-radius:50%;flex-shrink:0}
.STATUS-DOT.uploaded{background:#22C55E}
.STATUS-DOT.transcribed{background:#3B82F6}
.STATUS-DOT.reviewed{background:#A855F7}
.STATUS-DOT.archived{background:#6B7280}
.STATUS-DOT.placeholder{background:#F59E0B}
.STATUS-LABEL{font-size:6.5px;text-transform:uppercase;letter-spacing:.4px;font-weight:700}
.BUBBLE.sender .STATUS-LABEL{color:rgba(255,255,255,.65)}
.BUBBLE.receiver .STATUS-LABEL{color:#666}
/* Year divider */
.YEAR-DIV{display:flex;align-items:center;padding:4px 0;margin:2px 0}
.YEAR-LINE{flex:1;border-top:1px solid #D1CCC8}
.YEAR-LABEL{font-size:7px;font-weight:800;color:#AAA;text-transform:uppercase;letter-spacing:1px;padding:0 8px}
/* Summary bar */
.SUMBAR{display:flex;align-items:center;justify-content:space-between;padding:0 0 8px;margin-bottom:0}
.SUMBAR-count{font-size:9px;font-weight:600;color:#888}
.SUMBAR-link{font-size:8px;font-weight:700;color:#002850;display:flex;align-items:center;gap:3px}
/* Empty states */
.EMPTY{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:24px 12px;text-align:center;border:1.5px dashed #D1CCC8;border-radius:4px;background:#F7F5F2}
.EMPTY-ICON{width:28px;height:28px;border-radius:50%;background:#E8E4DF;display:flex;align-items:center;justify-content:center;font-size:14px;margin-bottom:8px}
.EMPTY-H{font-size:10px;font-weight:700;color:#444;font-family:Georgia,serif;margin-bottom:3px}
.EMPTY-S{font-size:8px;color:#999;line-height:1.4}
.EMPTY-sm{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 12px;text-align:center;border:1.5px solid #E0DDD6;border-radius:4px;background:#F7F5F2}
/* Annotation callouts */
.ANN{position:absolute;background:#FFFBEB;border:1px solid #FDE68A;border-radius:4px;padding:3px 6px;font-size:7.5px;color:#92400E;font-weight:600;line-height:1.3;white-space:nowrap;z-index:10}
.ANN-line{position:absolute;border-left:1px dashed #FDE68A;z-index:9}
/* Spec tables */
.BP-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px}
.BP-table th{text-align:left;padding:8px 12px;font-size:10px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid #E8E4DF;background:#F7F5F2}
.BP-table td{padding:8px 12px;border-bottom:1px solid #F0EDE6;color:#444;vertical-align:top}
.BP-table tr:last-child td{border-bottom:none}
.BP-table code{font-size:10px;font-family:monospace;background:#F3F4F6;padding:1px 4px;border-radius:2px}
.BP-table td:first-child{font-weight:700;white-space:nowrap}
/* Decision boxes */
.DECISION{background:#F0F7FF;border-left:4px solid #002850;border-radius:0 6px 6px 0;border-right:1px solid #BFDBFE;border-top:1px solid #BFDBFE;border-bottom:1px solid #BFDBFE;padding:14px 16px;margin:16px 0}
.DECISION-title{font-size:10px;font-weight:800;color:#002850;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
.DECISION ul{display:flex;flex-direction:column;gap:5px;padding-left:0}
.DECISION li{font-size:11px;color:#1E3A5F;line-height:1.5;list-style:none;padding-left:16px;position:relative}
.DECISION li::before{content:'→';position:absolute;left:0;color:#60A5FA;font-weight:700}
/* Impl notes */
.IMPL-RULES{display:flex;flex-direction:column;gap:0}
.IMPL-RULE{padding:12px 16px;border-bottom:1px solid #E8E4DF;display:grid;grid-template-columns:28px 1fr;gap:12px;align-items:start}
.IMPL-RULE:last-child{border-bottom:none}
.IMPL-RULE:nth-child(odd){background:#FAFAF7}
.IMPL-NUM{font-size:11px;font-weight:900;color:#002850;padding-top:1px}
.IMPL-BODY{}
.IMPL-TITLE{font-size:11px;font-weight:800;color:#1A1A1A;margin-bottom:3px}
.IMPL-TEXT{font-size:11px;color:#555;line-height:1.6}
.IMPL-TEXT code{font-size:10px;font-family:monospace;background:#F3F4F6;padding:1px 4px;border-radius:2px;color:#1A1A1A}
/* Before/after comparison */
.CMP{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1.5px solid #E0DDD6;border-radius:6px;overflow:hidden;margin-top:12px}
.CMP-col{padding:0}
.CMP-head{padding:10px 14px;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;border-bottom:1.5px solid #E0DDD6}
.CMP-head.before{background:#FFF5F5;color:#B91C1C;border-right:1px solid #E0DDD6}
.CMP-head.after{background:#F0FDF4;color:#166534}
.CMP-body{padding:12px 14px}
.CMP-col.before .CMP-body{border-right:1px solid #E0DDD6;background:#FFFAFA}
.CMP-col.after .CMP-body{background:#FAFFFE}
.CMP-item{font-size:11px;color:#444;padding:3px 0;line-height:1.5;display:flex;align-items:flex-start;gap:6px}
.CMP-item::before{content:'✕';color:#EF4444;font-size:10px;flex-shrink:0;margin-top:1px}
.CMP-col.after .CMP-item::before{content:'✓';color:#22C55E}
.CMP-item code{font-size:10px;font-family:monospace;background:#F3F4F6;padding:1px 4px;border-radius:2px}
/* File table */
.FILE-TABLE{width:100%;border-collapse:collapse;font-size:11px;margin-top:12px}
.FILE-TABLE th{text-align:left;padding:9px 14px;font-size:10px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid #E8E4DF;background:#F7F5F2}
.FILE-TABLE td{padding:9px 14px;border-bottom:1px solid #F0EDE6;color:#444;vertical-align:top}
.FILE-TABLE tr:last-child td{border-bottom:none}
.FILE-TABLE code{font-size:10px;font-family:monospace;background:#F3F4F6;padding:1px 4px;border-radius:2px}
.FILE-TABLE .lines{font-size:10px;font-weight:700;color:#002850}
.FILE-TABLE .delta-add{color:#166534;font-weight:700}
.FILE-TABLE .delta-rm{color:#B91C1C;font-weight:700}
/* General utility */
code{font-size:10px;font-family:monospace;background:#F3F4F6;padding:1px 4px;border-radius:2px;color:#1A1A1A}
.note{font-size:11px;color:#666;line-height:1.6;margin-bottom:10px}
.note strong{color:#1A1A1A}
.SPACER{height:1px;background:#E0DDD6;margin:14px 0}
.row-gap{display:flex;gap:20px;align-items:flex-start;margin-bottom:16px;flex-wrap:wrap}
/* Mobile wireframe shell */
.wf-m{background:#fff;border:2px solid #B8B4AE;border-radius:14px;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.08);width:160px;flex-shrink:0}
.wf-m .N-m{height:36px}
.mobile-status{height:8px;background:#0D2240;display:flex;align-items:center;justify-content:flex-end;padding:0 8px}
.mobile-status-dot{width:3px;height:3px;border-radius:50%;background:rgba(255,255,255,.4);margin-left:2px}
/* Tablet wireframe */
.wf-t{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.08);width:260px;flex-shrink:0}
/* i18n table */
.I18N-TABLE{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px}
.I18N-TABLE th{text-align:left;padding:7px 12px;font-size:10px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid #E8E4DF;background:#F7F5F2}
.I18N-TABLE td{padding:7px 12px;border-bottom:1px solid #F0EDE6;vertical-align:top}
.I18N-TABLE code{font-family:monospace;font-size:10px;background:#F3F4F6;padding:1px 4px;border-radius:2px}
.I18N-TABLE .de{color:#1A1A1A;font-weight:600}
.I18N-TABLE .en{color:#444}
.I18N-TABLE .es{color:#666}
</style>
</head>
<body>
<div class="doc">
<!-- ═══════════════════════════════════════════ MASTHEAD ═══════════════════════ -->
<div class="masthead">
<div class="masthead-left">
<h1>Conversations — Narrow Column Redesign</h1>
<p>Developer specification for the WhatsApp/SMS-inspired chat layout. Constrains all bubble content to a centred 640 px column so opposing bubbles are close together — familiar to older users, easier to follow on wide screens.</p>
<div class="masthead-meta">
<span class="masthead-tag v-badge">v1.0 · 2026-03-29</span>
<span class="masthead-tag file-badge">3 files changed</span>
<span class="masthead-tag issue-badge">feature/153-notification-history branch</span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 0 — ARCHITECTURE ══════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">0</div>
<div class="sec-meta">
<span class="sec-badge badge-arch">Architecture</span>
<div class="sec-title">Files, routes, and constraints</div>
<div class="sec-tagline">Three files. One route. No new dependencies. All changes are presentational — the data layer, server load functions, and URL query-param contract are untouched.</div>
</div>
</div>
<table class="FILE-TABLE">
<thead>
<tr>
<th>File</th>
<th>What changes</th>
<th>Approx. lines touched</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>frontend/src/routes/conversations/ConversationFilterBar.svelte</code></td>
<td>Add <code>expanded</code> bindable prop. Render either a collapsed strip or the full form depending on state. Add "Adjust" button. Add "Apply"/"Cancel" controls for overlay mode.</td>
<td class="lines"><span class="delta-add">+38</span> / <span class="delta-rm">2</span></td>
</tr>
<tr>
<td><code>frontend/src/routes/conversations/ConversationTimeline.svelte</code></td>
<td>Remove central vertical line. Wrap bubble list in <code>max-w-[640px] mx-auto</code> column. Change bubble max-width. Add status text labels alongside status dots.</td>
<td class="lines"><span class="delta-add">+14</span> / <span class="delta-rm">6</span></td>
</tr>
<tr>
<td><code>frontend/src/routes/conversations/+page.svelte</code></td>
<td>Add <code>filterExpanded</code> reactive state. Auto-collapse when both persons are selected and documents load. Pass <code>bind:expanded</code> to FilterBar.</td>
<td class="lines"><span class="delta-add">+8</span> / <span class="delta-rm">1</span></td>
</tr>
<tr>
<td><code>frontend/src/lib/paraglide/messages/*.json</code> (de / en / es)</td>
<td>Add 6 new i18n keys: <code>conv_filter_adjust</code> + 5 status label keys.</td>
<td class="lines"><span class="delta-add">+6</span> per file</td>
</tr>
</tbody>
</table>
<div class="DECISION">
<div class="DECISION-title">🏗 Architecture decisions</div>
<ul>
<li>Route stays at <code>/conversations</code> — query params (<code>senderId</code>, <code>receiverId</code>, <code>from</code>, <code>to</code>, <code>dir</code>) are the only data contract. No changes to <code>+page.server.ts</code>.</li>
<li>The <code>filterExpanded</code> flag lives in <code>+page.svelte</code> (not FilterBar) so the parent controls auto-collapse behaviour after navigation.</li>
<li>No JS animation libraries. Collapse uses a CSS <code>transition: max-height</code> or a Svelte <code>#if</code> block — developer's choice, but the spec uses <code>#if</code> for simplicity.</li>
<li>Breakpoints: mobile 375 px, tablet 768 px, desktop 1440 px. The 640 px column is achieved with Tailwind <code>max-w-[640px]</code> — no new CSS variables needed.</li>
</ul>
</div>
<p class="note" style="margin-top:12px"><strong>Unchanged:</strong> <code>+page.server.ts</code>, the API client, all repository/service code, URL query-param names, Paraglide message IDs already in use, canWrite guard, and all existing test IDs (<code>data-testid</code>).</p>
</div>
<!-- ═══════════════════════════════════════ SECTION 1 — FILTER BAR ════════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">1</div>
<div class="sec-meta">
<span class="sec-badge badge-filter">Filter Bar</span>
<div class="sec-title">Collapsible filter bar — 4 states</div>
<div class="sec-tagline">When no conversation is active the full form stays visible (nothing to collapse yet). Once both persons are selected and results load, the bar auto-collapses to a single-line strip. Tapping "Adjust" re-expands it as an inline overlay.</div>
</div>
</div>
<!-- 1a + 1b side by side -->
<div class="screens cols2" style="margin-bottom:24px">
<!-- 1a: Desktop expanded (no conversation) -->
<div class="screen-block">
<div class="screen-label">1a <span class="sz">Desktop 1440</span> <span class="state-tag">No conversation selected</span></div>
<div class="wf" style="width:100%">
<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/conversations</span></div></div>
<div class="N">
<div class="logo">FAMILIENARCHIV</div>
<div class="nl">Dokumente</div><div class="nl on">Gespräche</div><div class="nl">Personen</div>
<div class="nr"><div class="nav-ico"></div><div class="nav-user">MA</div></div>
</div>
<div class="MAIN">
<div class="PAGE-HEAD">
<div class="PAGE-H1">Gespräche</div>
<div class="PAGE-SUB">Briefwechsel zwischen Familienmitgliedern</div>
</div>
<!-- Full filter bar -->
<div class="FB-FULL">
<div class="FB-ROW1">
<div>
<div class="FB-LABEL">Person A</div>
<div class="FB-INPUT placeholder">Name eingeben…</div>
</div>
<div class="FB-SWAP"></div>
<div>
<div class="FB-LABEL">Person B</div>
<div class="FB-INPUT placeholder">Name eingeben…</div>
</div>
</div>
<div class="FB-ROW2">
<div>
<div class="FB-LABEL">Von Datum</div>
<div class="FB-INPUT placeholder">TT.MM.JJJJ</div>
</div>
<div>
<div class="FB-LABEL">Bis Datum</div>
<div class="FB-INPUT placeholder">TT.MM.JJJJ</div>
</div>
<div>
<div class="FB-LABEL">&nbsp;</div>
<div class="FB-SORT">Sortierung: Neueste zuerst ▾</div>
</div>
</div>
<div style="margin-top:6px;font-size:8px;color:#AAA;font-style:italic">↑ No collapse button — conversation not yet active</div>
</div>
<!-- Empty state -->
<div class="EMPTY" style="padding:32px">
<div class="EMPTY-ICON">👥</div>
<div class="EMPTY-H">Wähle zwei Personen aus</div>
<div class="EMPTY-S">Wähle Person A und Person B aus, um ihre Korrespondenz anzuzeigen</div>
</div>
</div>
</div>
<div class="screen-cap">Full filter form always expanded when senderId or receiverId is absent. No collapse/strip needed — there is no conversation to preserve context from.</div>
</div>
<!-- 1b: Desktop collapsed strip (conversation active) -->
<div class="screen-block">
<div class="screen-label">1b <span class="sz">Desktop 1440</span> <span class="state-tag collapsed">Conversation active · collapsed</span></div>
<div class="wf" style="width:100%;position:relative">
<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/conversations?senderId=…&amp;receiverId=…</span></div></div>
<div class="N">
<div class="logo">FAMILIENARCHIV</div>
<div class="nl">Dokumente</div><div class="nl on">Gespräche</div><div class="nl">Personen</div>
<div class="nr"><div class="nav-ico"></div><div class="nav-user">MA</div></div>
</div>
<div class="MAIN">
<div class="PAGE-HEAD">
<div class="PAGE-H1">Gespräche</div>
<div class="PAGE-SUB">Briefwechsel zwischen Familienmitgliedern</div>
</div>
<!-- Collapsed strip -->
<div class="FB-STRIP">
<div class="FB-STRIP-left">
<div>
<div class="FB-STRIP-names">Anna Müller ⇄ Heinrich Raddatz</div>
<div class="FB-STRIP-meta">47 Dokumente · 19281965</div>
</div>
</div>
<div class="FB-STRIP-btn">Anpassen</div>
</div>
<!-- Summary bar -->
<div class="SUMBAR">
<div class="SUMBAR-count">47 Dokumente · 19281965</div>
<div class="SUMBAR-link"> Neues Dokument</div>
</div>
<!-- Narrow column chat -->
<div class="CHAT-OUTER">
<div class="CHAT-COL">
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL">1928</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right">
<div class="AVATAR sender">AM</div>
<div class="BUBBLE sender">
<div class="BUBBLE-TITLE">Brief an Heinrich, April 1928</div>
<div class="BUBBLE-META">12.04.1928 · München</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT uploaded"></div><div class="STATUS-LABEL" style="color:rgba(255,255,255,.65)">Hochgeladen</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left">
<div class="AVATAR receiver">HR</div>
<div class="BUBBLE receiver">
<div class="BUBBLE-TITLE">Antwort vom 3. Mai</div>
<div class="BUBBLE-META">03.05.1928 · Berlin</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT transcribed"></div><div class="STATUS-LABEL">Transkribiert</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right">
<div class="AVATAR sender">AM</div>
<div class="BUBBLE sender">
<div class="BUBBLE-TITLE">Postkarte, Sommer 1928</div>
<div class="BUBBLE-META">15.07.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT reviewed"></div><div class="STATUS-LABEL" style="color:rgba(255,255,255,.65)">Geprüft</div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="screen-cap">Auto-collapsed to single strip after results load. "Anpassen" button re-expands. Year + count visible in strip for quick orientation without opening filters.</div>
</div>
</div>
<!-- 1c + 1d -->
<div class="screens cols2">
<!-- 1c: Expanded overlay (Adjust clicked) -->
<div class="screen-block">
<div class="screen-label">1c <span class="sz">Desktop 1440</span> <span class="state-tag active">Overlay expanded after "Adjust"</span></div>
<div class="wf" style="width:100%">
<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/conversations?senderId=…</span></div></div>
<div class="N">
<div class="logo">FAMILIENARCHIV</div>
<div class="nl">Dokumente</div><div class="nl on">Gespräche</div>
<div class="nr"><div class="nav-ico"></div></div>
</div>
<div class="MAIN">
<div class="PAGE-HEAD"><div class="PAGE-H1">Gespräche</div></div>
<!-- Still show strip (context) -->
<div class="FB-STRIP" style="opacity:.5">
<div class="FB-STRIP-left"><div class="FB-STRIP-names">Anna Müller ⇄ Heinrich Raddatz</div></div>
<div class="FB-STRIP-btn" style="opacity:.4">Anpassen</div>
</div>
<!-- Overlay form drops below strip -->
<div class="FB-OVERLAY">
<div style="font-size:8px;font-weight:800;color:#002850;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Filter anpassen</div>
<div class="FB-ROW1">
<div>
<div class="FB-LABEL">Person A</div>
<div class="FB-INPUT">Anna Müller</div>
</div>
<div class="FB-SWAP"></div>
<div>
<div class="FB-LABEL">Person B</div>
<div class="FB-INPUT">Heinrich Raddatz</div>
</div>
</div>
<div class="FB-ROW2">
<div>
<div class="FB-LABEL">Von Datum</div>
<div class="FB-INPUT placeholder"></div>
</div>
<div>
<div class="FB-LABEL">Bis Datum</div>
<div class="FB-INPUT placeholder"></div>
</div>
<div>
<div class="FB-LABEL">&nbsp;</div>
<div class="FB-SORT">Neueste zuerst ▾</div>
</div>
</div>
<div class="FB-APPLY">Anwenden</div>
<div class="FB-CANCEL">Abbrechen</div>
</div>
<!-- Blurred chat behind overlay -->
<div class="CHAT-OUTER" style="opacity:.3;pointer-events:none">
<div class="CHAT-COL">
<div class="BUBBLE-ROW right"><div class="BUBBLE-GROUP right"><div class="BUBBLE sender"><div class="BUBBLE-TITLE">Brief an Heinrich…</div></div></div></div>
</div>
</div>
</div>
</div>
<div class="screen-cap">Overlay drops inline below the strip (not a floating modal). Strip remains visible at reduced opacity. "Abbrechen" dismisses back to collapsed. "Anwenden" fires applyFilters() and collapses.</div>
</div>
<!-- 1d: Mobile filter bar -->
<div class="screen-block">
<div class="screen-label">1d <span class="sz">Mobile 375</span> <span class="state-tag mobile">Mobile — both states</span></div>
<div style="display:flex;gap:20px;align-items:flex-start">
<!-- Mobile collapsed -->
<div>
<div style="font-size:9px;font-weight:700;color:#888;text-align:center;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">Collapsed</div>
<div class="wf-m">
<div class="mobile-status"><div class="mobile-status-dot"></div><div class="mobile-status-dot"></div><div class="mobile-status-dot"></div></div>
<div class="N-m"><div class="logo" style="font-size:8px">FAMILIENARCHIV</div><div class="nav-ico"></div></div>
<div class="MAIN-sm">
<div style="font-size:9px;font-weight:800;color:#0D2240;font-family:Georgia,serif;border-bottom:1px solid rgba(0,0,0,.08);padding-bottom:5px;margin-bottom:4px">Gespräche</div>
<div class="FB-STRIP" style="margin:0 -14px;padding:6px 10px;background:#F0EDE8;border-top:1px solid #DDD8D0;border-bottom:1px solid #DDD8D0;display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<div>
<div style="font-size:8px;font-weight:700;color:#0D2240;font-family:Georgia,serif">A. Müller → H. Raddatz</div>
<div style="font-size:7px;color:#999">47 Dok. · 19281965</div>
</div>
<div style="font-size:7px;font-weight:800;color:#002850;border:1px solid #002850;padding:2px 5px;border-radius:2px;text-transform:uppercase">Anpassen</div>
</div>
<div class="CHAT-OUTER" style="padding:8px">
<div class="CHAT-COL-FULL" style="gap:4px">
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL">1928</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right" style="max-width:82%">
<div class="BUBBLE sender" style="padding:5px 7px">
<div class="BUBBLE-TITLE">Brief, April 1928</div>
<div class="BUBBLE-META">12.04.1928</div>
</div>
</div>
</div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left" style="max-width:82%">
<div class="BUBBLE receiver" style="padding:5px 7px">
<div class="BUBBLE-TITLE">Antwort, Mai 1928</div>
<div class="BUBBLE-META">03.05.1928</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile expanded -->
<div>
<div style="font-size:9px;font-weight:700;color:#888;text-align:center;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px">Expanded</div>
<div class="wf-m">
<div class="mobile-status"><div class="mobile-status-dot"></div><div class="mobile-status-dot"></div><div class="mobile-status-dot"></div></div>
<div class="N-m"><div class="logo" style="font-size:8px">FAMILIENARCHIV</div><div class="nav-ico"></div></div>
<div class="MAIN-sm" style="gap:6px">
<div style="font-size:9px;font-weight:800;color:#0D2240;font-family:Georgia,serif;border-bottom:1px solid rgba(0,0,0,.08);padding-bottom:5px">Gespräche</div>
<div style="border:1px solid #C8C4BE;background:#F7F5F2;padding:10px">
<div class="FB-LABEL">Person A</div>
<div class="FB-INPUT" style="height:24px;font-size:8px;margin-bottom:8px">Anna Müller</div>
<div style="text-align:center;margin-bottom:8px;font-size:9px;color:#888">⇅ Tauschen</div>
<div class="FB-LABEL">Person B</div>
<div class="FB-INPUT" style="height:24px;font-size:8px;margin-bottom:8px">Heinrich Raddatz</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:8px">
<div>
<div class="FB-LABEL">Von</div>
<div class="FB-INPUT placeholder" style="height:24px;font-size:8px"></div>
</div>
<div>
<div class="FB-LABEL">Bis</div>
<div class="FB-INPUT placeholder" style="height:24px;font-size:8px"></div>
</div>
</div>
<div class="FB-SORT" style="height:24px;font-size:7px;margin-bottom:6px">Neueste zuerst ▾</div>
<div class="FB-APPLY" style="height:26px;font-size:8px">Anwenden</div>
<div class="FB-CANCEL">Abbrechen</div>
</div>
</div>
</div>
</div>
</div>
<div class="screen-cap">Mobile: full-width tap target on collapsed strip. Expanded mode stacks all fields vertically. Swap button sits between Person A and B. "Abbrechen" collapses without navigating.</div>
</div>
</div>
<div class="DECISION" style="margin-top:20px">
<div class="DECISION-title">📐 Filter bar prop contract (updated)</div>
<ul>
<li>Add <code>expanded = $bindable(true)</code> to FilterBar's prop definition. Default <code>true</code> so existing tests that don't pass the prop still see the full form.</li>
<li>In <code>+page.svelte</code>: <code>let filterExpanded = $state(!data.filters.senderId || !data.filters.receiverId)</code>. After successful load with results, set <code>filterExpanded = false</code> inside the <code>$effect</code> that syncs filter values.</li>
<li>"Anwenden" inside the expanded overlay calls <code>onapplyFilters()</code> then sets <code>expanded = false</code> (local) — the parent's bound state updates via Svelte 5 bindable.</li>
<li>"Abbrechen" sets <code>expanded = false</code> without calling <code>onapplyFilters()</code>.</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 2 — CHAT COLUMN ══════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">2</div>
<div class="sec-meta">
<span class="sec-badge badge-chat">Chat Column</span>
<div class="sec-title">Narrow column layout — 3 breakpoints</div>
<div class="sec-tagline">The structural change. All bubble content is centred in a <code>max-w-[640px]</code> column inside the existing chat container. The outer container (border, shadow, surface bg) stays at full page width. No central dividing line.</div>
</div>
</div>
<!-- Concept diagram -->
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;padding:20px;margin-bottom:24px;display:flex;gap:40px;align-items:center">
<!-- Before diagram -->
<div style="flex:1">
<div style="font-size:10px;font-weight:800;color:#B91C1C;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Before — full-width split</div>
<div style="background:#F7F5F2;border:1px solid #D1CCC8;border-radius:4px;padding:12px;position:relative;height:140px;overflow:hidden">
<!-- Central line -->
<div style="position:absolute;top:0;bottom:0;left:50%;width:1px;background:#D1CCC8;opacity:.6"></div>
<!-- Right bubble (wide) -->
<div style="position:absolute;right:8px;top:14px;width:44%;background:#002850;border-radius:6px 6px 2px 6px;padding:6px 8px">
<div style="font-size:7px;color:#fff;font-weight:600">Brief, April 1928</div>
<div style="font-size:6px;color:rgba(255,255,255,.6);margin-top:2px">12.04.1928</div>
</div>
<!-- Left bubble (wide) -->
<div style="position:absolute;left:8px;top:60px;width:44%;background:#E8E4DF;border-radius:6px 6px 6px 2px;padding:6px 8px">
<div style="font-size:7px;color:#333;font-weight:600">Antwort, Mai 1928</div>
<div style="font-size:6px;color:#888;margin-top:2px">03.05.1928</div>
</div>
<!-- Right bubble 2 -->
<div style="position:absolute;right:8px;top:106px;width:38%;background:#002850;border-radius:6px 6px 2px 6px;padding:6px 8px">
<div style="font-size:7px;color:#fff;font-weight:600">Postkarte, Sommer</div>
<div style="font-size:6px;color:rgba(255,255,255,.6);margin-top:2px">15.07.1928</div>
</div>
<!-- Gap annotation -->
<div style="position:absolute;top:36px;left:50%;transform:translateX(-50%);font-size:8px;color:#B91C1C;font-weight:700;white-space:nowrap">← wide gap →</div>
</div>
</div>
<!-- Arrow -->
<div style="font-size:28px;color:#D1CCC8"></div>
<!-- After diagram -->
<div style="flex:1">
<div style="font-size:10px;font-weight:800;color:#166534;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">After — narrow 640 px column</div>
<div style="background:#F7F5F2;border:1px solid #D1CCC8;border-radius:4px;padding:12px;position:relative;height:140px;overflow:hidden;display:flex;flex-direction:column;justify-content:center">
<!-- Narrow column indicator -->
<div style="max-width:60%;margin:0 auto;width:100%;display:flex;flex-direction:column;gap:5px">
<div style="display:flex;justify-content:flex-end">
<div style="width:55%;background:#002850;border-radius:6px 6px 2px 6px;padding:5px 7px">
<div style="font-size:7px;color:#fff;font-weight:600">Brief, April 1928</div>
<div style="font-size:6px;color:rgba(255,255,255,.6);margin-top:1px">12.04.1928</div>
</div>
</div>
<div style="display:flex;justify-content:flex-start">
<div style="width:55%;background:#E8E4DF;border-radius:6px 6px 6px 2px;padding:5px 7px">
<div style="font-size:7px;color:#333;font-weight:600">Antwort, Mai 1928</div>
<div style="font-size:6px;color:#888;margin-top:1px">03.05.1928</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end">
<div style="width:48%;background:#002850;border-radius:6px 6px 2px 6px;padding:5px 7px">
<div style="font-size:7px;color:#fff;font-weight:600">Postkarte, Sommer</div>
<div style="font-size:6px;color:rgba(255,255,255,.6);margin-top:1px">15.07.1928</div>
</div>
</div>
</div>
<!-- Width annotation -->
<div style="position:absolute;bottom:4px;left:50%;transform:translateX(-50%);font-size:7px;color:#166534;font-weight:700;white-space:nowrap">max-w-[640px] · mx-auto</div>
</div>
</div>
</div>
<div class="screens" style="gap:20px;flex-wrap:nowrap;align-items:flex-start">
<!-- 2a: Desktop -->
<div class="screen-block" style="flex:2;min-width:0">
<div class="screen-label">2a <span class="sz">Desktop 1440</span></div>
<div class="wf" style="width:100%">
<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/conversations?senderId=a1&amp;receiverId=b2</span></div></div>
<div class="N"><div class="logo">FAMILIENARCHIV</div><div class="nl">Dokumente</div><div class="nl on">Gespräche</div><div class="nl">Personen</div><div class="nr"><div class="nav-ico"></div><div class="nav-user">MA</div></div></div>
<div class="MAIN" style="gap:8px">
<div class="PAGE-HEAD"><div class="PAGE-H1">Gespräche</div></div>
<div class="FB-STRIP"><div class="FB-STRIP-left"><div><div class="FB-STRIP-names">Anna Müller ⇄ Heinrich Raddatz</div><div class="FB-STRIP-meta">47 Dokumente · 19281965</div></div></div><div class="FB-STRIP-btn">Anpassen</div></div>
<div class="SUMBAR"><div class="SUMBAR-count">47 Dokumente · 19281965</div><div class="SUMBAR-link"> Neues Dokument</div></div>
<!-- Chat outer at full width, column centred inside -->
<div class="CHAT-OUTER" style="padding:14px 20px">
<!-- Narrow column centred -->
<div style="max-width:340px;margin:0 auto;display:flex;flex-direction:column;gap:6px">
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL">1928</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right">
<div class="AVATAR sender">AM</div>
<div class="BUBBLE sender">
<div class="BUBBLE-TITLE">Brief an Heinrich, April 1928</div>
<div class="BUBBLE-META">12.04.1928 · München</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT uploaded"></div><div class="STATUS-LABEL" style="color:rgba(255,255,255,.65)">Hochgeladen</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left">
<div class="AVATAR receiver">HR</div>
<div class="BUBBLE receiver">
<div class="BUBBLE-TITLE">Antwort vom 3. Mai 1928</div>
<div class="BUBBLE-META">03.05.1928 · Berlin</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT transcribed"></div><div class="STATUS-LABEL">Transkribiert</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right">
<div class="AVATAR sender">AM</div>
<div class="BUBBLE sender">
<div class="BUBBLE-TITLE">Postkarte, Sommer 1928</div>
<div class="BUBBLE-META">15.07.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT reviewed"></div><div class="STATUS-LABEL" style="color:rgba(255,255,255,.65)">Geprüft</div></div>
</div>
</div>
</div>
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL">1929</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left">
<div class="AVATAR receiver">HR</div>
<div class="BUBBLE receiver">
<div class="BUBBLE-TITLE">Neujahrskarte 1929</div>
<div class="BUBBLE-META">01.01.1929</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT archived"></div><div class="STATUS-LABEL">Archiviert</div></div>
</div>
</div>
</div>
</div>
<!-- Annotation: column width -->
<div style="text-align:center;margin-top:8px;font-size:8px;color:#AAA;font-style:italic">⟵ outer container: full page width (max-w-5xl) · chat column: max-w-[640px] centred ⟶</div>
</div>
</div>
</div>
<div class="screen-cap">Desktop 1440 px. Chat outer container spans to page max-w-5xl (unchanged). Bubble column is max-w-[640px] mx-auto inside. The grey padding on each side is empty space — visually frames the conversation. No central line.</div>
</div>
<!-- 2b: Tablet -->
<div class="screen-block" style="flex:1;min-width:220px">
<div class="screen-label">2b <span class="sz">Tablet 768</span></div>
<div class="wf-t">
<div class="wf-bar" style="height:20px"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div></div>
<div class="N" style="height:36px;padding:0 10px"><div class="logo" style="font-size:8px">FAMILIENARCHIV</div><div class="nl" style="font-size:7px">Gespräche</div><div class="nr"><div class="nav-ico" style="width:18px;height:18px"></div></div></div>
<div class="MAIN-sm">
<div class="FB-STRIP" style="margin:0 -14px;padding:5px 10px;background:#F0EDE8;border-bottom:1px solid #DDD8D0;margin-bottom:6px">
<div class="FB-STRIP-left"><div><div style="font-size:9px;font-weight:700;color:#0D2240;font-family:Georgia,serif">A. Müller ⇄ H. Raddatz</div><div style="font-size:7px;color:#999">47 Dok. · 19281965</div></div></div>
<div style="font-size:7px;font-weight:800;color:#002850;border:1px solid #002850;padding:2px 5px;border-radius:2px;text-transform:uppercase">Anpassen</div>
</div>
<div style="font-size:8px;color:#888;margin-bottom:4px">47 Dokumente · 19281965</div>
<div class="CHAT-OUTER" style="padding:8px">
<div style="max-width:220px;margin:0 auto;display:flex;flex-direction:column;gap:5px">
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL">1928</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right">
<div class="AVATAR sender" style="width:16px;height:16px;font-size:5px">AM</div>
<div class="BUBBLE sender" style="padding:5px 7px">
<div class="BUBBLE-TITLE">Brief an Heinrich, April</div>
<div class="BUBBLE-META">12.04.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT uploaded"></div><div class="STATUS-LABEL" style="color:rgba(255,255,255,.6)">Hochgeladen</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left">
<div class="AVATAR receiver" style="width:16px;height:16px;font-size:5px">HR</div>
<div class="BUBBLE receiver" style="padding:5px 7px">
<div class="BUBBLE-TITLE">Antwort, Mai 1928</div>
<div class="BUBBLE-META">03.05.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT transcribed"></div><div class="STATUS-LABEL">Transkribiert</div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="screen-cap">Tablet 768 px. Column narrows to max-w-[560px]. Avatars visible (sm:flex). Filter strip collapsed.</div>
</div>
<!-- 2c: Mobile -->
<div class="screen-block" style="flex:0 0 auto">
<div class="screen-label">2c <span class="sz">Mobile 375</span></div>
<div class="wf-m">
<div class="mobile-status"></div>
<div class="N-m" style="height:36px"><div class="logo" style="font-size:8px">FAMILIENARCHIV</div><div class="nav-ico"></div></div>
<div class="MAIN-sm" style="padding:8px 10px;gap:6px">
<div style="font-size:9px;font-weight:800;color:#0D2240;font-family:Georgia,serif;padding-bottom:5px;border-bottom:1px solid rgba(0,0,0,.08)">Gespräche</div>
<div style="background:#F0EDE8;border-bottom:1px solid #DDD8D0;padding:5px 8px;margin:0 -10px;display:flex;align-items:center;justify-content:space-between">
<div style="font-size:8px;font-weight:700;color:#0D2240;font-family:Georgia,serif">A. Müller → H. Raddatz</div>
<div style="font-size:7px;font-weight:800;color:#002850;border:1px solid #002850;padding:2px 4px;border-radius:2px;text-transform:uppercase">Anpassen</div>
</div>
<div class="CHAT-OUTER" style="padding:6px">
<!-- Mobile: column is naturally full width -->
<div style="display:flex;flex-direction:column;gap:5px">
<div class="YEAR-DIV"><div class="YEAR-LINE"></div><div class="YEAR-LABEL" style="font-size:6px">1928</div><div class="YEAR-LINE"></div></div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right" style="max-width:82%">
<!-- No avatar on mobile -->
<div class="BUBBLE sender" style="padding:5px 7px">
<div class="BUBBLE-TITLE" style="font-size:7.5px">Brief an Heinrich, April 1928</div>
<div class="BUBBLE-META" style="font-size:6px">12.04.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT uploaded"></div><div class="STATUS-LABEL" style="font-size:6px;color:rgba(255,255,255,.6)">Hochgeladen</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left" style="max-width:82%">
<div class="BUBBLE receiver" style="padding:5px 7px">
<div class="BUBBLE-TITLE" style="font-size:7.5px">Antwort, Mai 1928</div>
<div class="BUBBLE-META" style="font-size:6px">03.05.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT transcribed"></div><div class="STATUS-LABEL" style="font-size:6px">Transkribiert</div></div>
</div>
</div>
</div>
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right" style="max-width:82%">
<div class="BUBBLE sender" style="padding:5px 7px">
<div class="BUBBLE-TITLE" style="font-size:7.5px">Postkarte, Sommer</div>
<div class="BUBBLE-META" style="font-size:6px">15.07.1928</div>
<div class="BUBBLE-STATUS"><div class="STATUS-DOT reviewed"></div><div class="STATUS-LABEL" style="font-size:6px;color:rgba(255,255,255,.6)">Geprüft</div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="screen-cap">Mobile 375 px. No avatars (hidden sm:flex). Bubbles max-w-[85%]. Column fills full width naturally — no mx-auto padding needed.</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 3 — BUBBLE CARD ══════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">3</div>
<div class="sec-meta">
<span class="sec-badge badge-bubble">Bubble Card</span>
<div class="sec-title">Bubble card anatomy — sender &amp; receiver</div>
<div class="sec-tagline">The cards are links (<code>&lt;a href="/documents/{id}"&gt;</code>). The key WCAG fix: status indicator gains a text label next to the coloured dot so colour is no longer the sole information channel.</div>
</div>
</div>
<div class="screens cols2">
<!-- Sender bubble -->
<div class="screen-block">
<div class="screen-label">3a — Sender bubble (right-aligned, navy)</div>
<div style="background:#F7F5F2;border:1.5px solid #E0DDD6;border-radius:6px;padding:24px;position:relative">
<!-- Rendered bubble -->
<div style="display:flex;justify-content:flex-end;margin-bottom:20px">
<div style="display:flex;gap:8px;flex-direction:row-reverse;max-width:75%">
<div class="AVATAR sender" style="width:28px;height:28px;font-size:9px;align-self:flex-end;margin-bottom:2px">AM</div>
<div class="BUBBLE sender" style="padding:10px 12px;font-size:1px">
<div class="BUBBLE-TITLE" style="font-size:10px;margin-bottom:4px">Brief an Heinrich Raddatz, April 1928</div>
<div class="BUBBLE-META" style="font-size:9px;margin-bottom:5px">12.04.1928 · München</div>
<div class="BUBBLE-STATUS" style="gap:4px">
<div class="STATUS-DOT uploaded" style="width:6px;height:6px"></div>
<div class="STATUS-LABEL" style="font-size:8px;color:rgba(255,255,255,.65)">Hochgeladen</div>
</div>
</div>
</div>
</div>
<!-- Annotated breakdown -->
<div style="border-top:1px dashed #C8C4BE;padding-top:14px">
<table style="width:100%;font-size:10px;border-collapse:collapse">
<tr><td style="padding:3px 0;color:#888;width:38%;vertical-align:top">Container</td><td style="padding:3px 0;color:#333"><code>max-w-[80%]</code> (was <code>md:max-w-[70%]</code>). No breakpoint prefix needed — column is already narrow.</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Corner cut</td><td style="padding:3px 0;color:#333"><code>rounded rounded-br-none</code> — WhatsApp-style tail at bottom-right</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Background</td><td style="padding:3px 0;color:#333"><code>bg-primary</code> (#002850 navy)</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Title</td><td style="padding:3px 0;color:#333"><code>font-serif text-sm font-medium text-primary-fg</code></td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Meta row</td><td style="padding:3px 0;color:#333"><code>font-sans text-[10px] tracking-wider uppercase text-primary-fg/70</code></td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Status — NEW</td><td style="padding:3px 0;color:#333"><code>flex items-center gap-1</code> → dot + text label (see note below)</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Hover</td><td style="padding:3px 0;color:#333"><code>hover:-translate-y-0.5 hover:shadow-md</code> — retained unchanged</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Avatar</td><td style="padding:3px 0;color:#333"><code>hidden sm:flex</code> — 32×32, navy fill, white initials</td></tr>
</table>
</div>
</div>
</div>
<!-- Receiver bubble -->
<div class="screen-block">
<div class="screen-label">3b — Receiver bubble (left-aligned, grey)</div>
<div style="background:#F7F5F2;border:1.5px solid #E0DDD6;border-radius:6px;padding:24px;position:relative">
<div style="display:flex;justify-content:flex-start;margin-bottom:20px">
<div style="display:flex;gap:8px;max-width:75%">
<div class="AVATAR receiver" style="width:28px;height:28px;font-size:9px;align-self:flex-end;margin-bottom:2px">HR</div>
<div class="BUBBLE receiver" style="padding:10px 12px">
<div class="BUBBLE-TITLE" style="font-size:10px;margin-bottom:4px">Antwort vom 3. Mai 1928</div>
<div class="BUBBLE-META" style="font-size:9px;margin-bottom:5px">03.05.1928 · Berlin</div>
<div class="BUBBLE-STATUS" style="gap:4px">
<div class="STATUS-DOT transcribed" style="width:6px;height:6px"></div>
<div class="STATUS-LABEL" style="font-size:8px;color:#444">Transkribiert</div>
</div>
</div>
</div>
</div>
<div style="border-top:1px dashed #C8C4BE;padding-top:14px">
<table style="width:100%;font-size:10px;border-collapse:collapse">
<tr><td style="padding:3px 0;color:#888;width:38%;vertical-align:top">Corner cut</td><td style="padding:3px 0;color:#333"><code>rounded rounded-bl-none</code> — tail at bottom-left</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Background</td><td style="padding:3px 0;color:#333"><code>bg-muted/50</code> (light grey, ~#E8E4DF)</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Title</td><td style="padding:3px 0;color:#333"><code>font-serif text-sm font-medium text-ink</code></td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Meta row</td><td style="padding:3px 0;color:#333"><code>font-sans text-[10px] tracking-wider uppercase text-ink-2</code></td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Status — NEW</td><td style="padding:3px 0;color:#333">Same structure, label colour <code>text-ink-2</code> (dark enough for contrast)</td></tr>
<tr><td style="padding:3px 0;color:#888;vertical-align:top">Avatar</td><td style="padding:3px 0;color:#333"><code>hidden sm:flex</code> — 32×32, white bg, ink initials, border</td></tr>
</table>
</div>
</div>
</div>
</div>
<!-- Status label table -->
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;padding:16px;margin-top:16px">
<div style="font-size:10px;font-weight:800;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px">Status dot → label mapping (ConversationTimeline.svelte)</div>
<table class="BP-table">
<thead><tr><th>DocumentStatus</th><th>Dot colour class</th><th>Label key (i18n)</th><th>de</th><th>Sender text colour</th><th>Receiver text colour</th></tr></thead>
<tbody>
<tr><td><code>PLACEHOLDER</code></td><td><code>bg-yellow-400</code></td><td><code>conv_status_placeholder</code></td><td>Platzhalter</td><td><code>text-primary-fg/60</code></td><td><code>text-ink-2</code></td></tr>
<tr><td><code>UPLOADED</code></td><td><code>bg-accent</code> (green)</td><td><code>conv_status_uploaded</code></td><td>Hochgeladen</td><td><code>text-primary-fg/65</code></td><td><code>text-ink-2</code></td></tr>
<tr><td><code>TRANSCRIBED</code></td><td><code>bg-blue-400</code></td><td><code>conv_status_transcribed</code></td><td>Transkribiert</td><td><code>text-primary-fg/65</code></td><td><code>text-ink-2</code></td></tr>
<tr><td><code>REVIEWED</code></td><td><code>bg-purple-400</code></td><td><code>conv_status_reviewed</code></td><td>Geprüft</td><td><code>text-primary-fg/65</code></td><td><code>text-ink-2</code></td></tr>
<tr><td><code>ARCHIVED</code></td><td><code>bg-gray-400</code></td><td><code>conv_status_archived</code></td><td>Archiviert</td><td><code>text-primary-fg/50</code></td><td><code>text-ink-3</code></td></tr>
</tbody>
</table>
<p style="font-size:10px;color:#888;margin-top:8px">Implement as a <code>const statusLabels: Record&lt;string, string&gt;</code> in <code>ConversationTimeline.svelte</code> using <code>m.conv_status_*</code> functions. Fall back to <code>doc.status</code> for unknown values.</p>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 4 — YEAR DIVIDERS ════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">4</div>
<div class="sec-meta">
<span class="sec-badge badge-divider">Year Dividers</span>
<div class="sec-title">Year dividers inside the narrow column</div>
<div class="sec-tagline">No HTML change needed. The existing <code>flex items-center</code> with <code>flex-grow border-t</code> lines auto-sizes to whatever container it sits in. Moving the bubble list inside the 640 px column automatically constrains dividers too.</div>
</div>
</div>
<div class="screens" style="align-items:flex-start">
<div class="screen-block" style="flex:1">
<div class="screen-label">4a — Year divider in narrow column</div>
<div style="background:#F7F5F2;border:1.5px solid #E0DDD6;border-radius:6px;padding:24px">
<div style="max-width:400px;margin:0 auto;display:flex;flex-direction:column;gap:10px">
<!-- Before year divider context -->
<div class="BUBBLE-ROW right">
<div class="BUBBLE-GROUP right" style="max-width:70%">
<div class="BUBBLE sender" style="padding:8px 10px"><div class="BUBBLE-TITLE" style="font-size:10px">Brief, Dezember 1928</div><div class="BUBBLE-META" style="font-size:8px">18.12.1928</div></div>
</div>
</div>
<!-- Year divider -->
<div class="YEAR-DIV" style="padding:6px 0">
<div class="YEAR-LINE"></div>
<div class="YEAR-LABEL" style="font-size:9px;padding:0 12px">1929</div>
<div class="YEAR-LINE"></div>
</div>
<!-- After year divider context -->
<div class="BUBBLE-ROW left">
<div class="BUBBLE-GROUP left" style="max-width:70%">
<div class="BUBBLE receiver" style="padding:8px 10px"><div class="BUBBLE-TITLE" style="font-size:10px">Neujahrskarte 1929</div><div class="BUBBLE-META" style="font-size:8px">01.01.1929</div></div>
</div>
</div>
</div>
<div style="border-top:1px dashed #C8C4BE;padding-top:12px;margin-top:16px;font-size:10px;color:#666;line-height:1.7">
<strong>Unchanged Svelte code:</strong><br>
<code>&lt;div class="relative flex items-center py-2 text-center"&gt;</code><br>
<code>&nbsp;&nbsp;&lt;div class="flex-grow border-t border-line"&gt;&lt;/div&gt;</code><br>
<code>&nbsp;&nbsp;&lt;span class="mx-4 font-sans text-xs font-bold tracking-widest text-ink/40 uppercase"&gt;{year}&lt;/span&gt;</code><br>
<code>&nbsp;&nbsp;&lt;div class="flex-grow border-t border-line"&gt;&lt;/div&gt;</code><br>
<code>&lt;/div&gt;</code><br><br>
The only structural change is that this element now lives inside the <code>max-w-[640px] mx-auto</code> wrapper div instead of the previous full-width flex column.
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 5 — SUMMARY BAR ══════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">5</div>
<div class="sec-meta">
<span class="sec-badge badge-summary">Summary Bar</span>
<div class="sec-title">Summary bar — above the narrow column</div>
<div class="sec-tagline">The summary bar (count + year range + new doc link) sits outside the narrow column at full page width. It remains at <code>max-w-5xl</code> because it is UI chrome, not conversation content.</div>
</div>
</div>
<div class="screen-block" style="max-width:700px">
<div class="screen-label">5a — Summary bar placement</div>
<div style="background:#ECEAE4;border:1.5px solid #E0DDD6;border-radius:6px;padding:16px">
<!-- Page max-w indicator -->
<div style="border:1px dashed #A6DAD8;border-radius:4px;padding:10px;margin-bottom:4px">
<div style="font-size:7.5px;color:#0F5B58;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">max-w-5xl (page column)</div>
<!-- Summary bar -->
<div style="display:flex;align-items:center;justify-content:space-between;padding:0 0 8px;border-bottom:1px solid #E0DDD6;margin-bottom:8px">
<span style="font-size:10px;font-weight:600;color:#888">47 Dokumente · 19281965</span>
<span style="font-size:9px;font-weight:700;color:#002850;display:flex;align-items:center;gap:3px"> Neues Dokument</span>
</div>
<!-- Chat outer -->
<div style="background:#F7F5F2;border:1px solid #D1CCC8;border-radius:3px;padding:10px;position:relative">
<div style="font-size:7.5px;color:#0F5B58;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;text-align:center">chat container — full width, bg-surface border shadow</div>
<!-- Narrow column indicator -->
<div style="border:1px dashed #002850;border-radius:3px;max-width:55%;margin:0 auto;padding:8px;text-align:center">
<div style="font-size:7.5px;color:#002850;font-weight:700;text-transform:uppercase;letter-spacing:.5px">max-w-[640px] · mx-auto</div>
<div style="font-size:7px;color:#888;margin-top:3px">bubble list lives here</div>
</div>
</div>
</div>
<p style="font-size:10px;color:#666;margin-top:8px;line-height:1.6">The <code>mb-4 flex items-center justify-between</code> summary div in <code>ConversationTimeline.svelte</code> is unchanged — it is already outside the chat container div. No code change needed for the summary bar itself.</p>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 6 — EMPTY STATES ═════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">6</div>
<div class="sec-meta">
<span class="sec-badge badge-empty">Empty States</span>
<div class="sec-title">Two empty state variants</div>
<div class="sec-tagline">The "no conversation selected" state keeps the full expanded filter bar (no strip). The "conversation selected but no results" state uses the collapsed strip and a compact empty message.</div>
</div>
</div>
<div class="screens cols2">
<!-- 6a: No conversation selected -->
<div class="screen-block">
<div class="screen-label">6a — No conversation selected (senderId or receiverId missing)</div>
<div class="wf" style="width:100%">
<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/conversations</span></div></div>
<div class="N"><div class="logo">FAMILIENARCHIV</div><div class="nl on">Gespräche</div><div class="nr"><div class="nav-ico"></div></div></div>
<div class="MAIN" style="gap:8px">
<div class="PAGE-HEAD"><div class="PAGE-H1">Gespräche</div><div class="PAGE-SUB">Briefwechsel zwischen Familienmitgliedern</div></div>
<div class="FB-FULL">
<div class="FB-ROW1">
<div><div class="FB-LABEL">Person A</div><div class="FB-INPUT placeholder">Name eingeben…</div></div>
<div class="FB-SWAP"></div>
<div><div class="FB-LABEL">Person B</div><div class="FB-INPUT">Heinrich Raddatz</div></div>
</div>
<div class="FB-ROW2">
<div><div class="FB-LABEL">Von Datum</div><div class="FB-INPUT placeholder">TT.MM.JJJJ</div></div>
<div><div class="FB-LABEL">Bis Datum</div><div class="FB-INPUT placeholder">TT.MM.JJJJ</div></div>
<div><div class="FB-LABEL">&nbsp;</div><div class="FB-SORT">Neueste zuerst ▾</div></div>
</div>
</div>
<div class="EMPTY">
<div class="EMPTY-ICON">👥</div>
<div class="EMPTY-H">Wähle zwei Personen aus</div>
<div class="EMPTY-S" style="max-width:220px">Wähle Person A und Person B aus, um ihren Briefwechsel anzuzeigen</div>
</div>
</div>
</div>
<div class="screen-cap">Full filter bar expanded (filterExpanded = true). "conv_empty_heading" / "conv_empty_text" message keys unchanged. Empty state uses dashed border to signal "waiting for input".</div>
</div>
<!-- 6b: Conversation selected, no results -->
<div class="screen-block">
<div class="screen-label">6b — Conversation selected, no results found</div>
<div class="wf" style="width:100%">
<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/conversations?senderId=a1&amp;receiverId=b2&amp;from=1960…</span></div></div>
<div class="N"><div class="logo">FAMILIENARCHIV</div><div class="nl on">Gespräche</div><div class="nr"><div class="nav-ico"></div></div></div>
<div class="MAIN" style="gap:8px">
<div class="PAGE-HEAD"><div class="PAGE-H1">Gespräche</div></div>
<div class="FB-STRIP">
<div class="FB-STRIP-left"><div><div class="FB-STRIP-names">Anna Müller ⇄ Heinrich Raddatz</div><div class="FB-STRIP-meta">Von 1960 · Neueste zuerst</div></div></div>
<div class="FB-STRIP-btn">Anpassen</div>
</div>
<div class="EMPTY-sm" style="padding:32px 12px">
<div style="font-size:12px;color:#888;margin-bottom:6px">🔍</div>
<div class="EMPTY-H">Keine Dokumente gefunden</div>
<div class="EMPTY-S" style="max-width:240px;margin-bottom:10px">Für diesen Zeitraum wurden keine Dokumente gefunden. Passe die Filter an.</div>
<div style="font-size:9px;font-weight:800;color:#002850;border:1.5px solid #002850;padding:5px 12px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px;cursor:pointer">Anpassen</div>
</div>
</div>
</div>
<div class="screen-cap">Collapsed strip still shows who the conversation is between. Compact empty state with an inline "Anpassen" CTA that triggers filterExpanded = true. Uses "conv_no_results_heading" / "conv_no_results_text" keys unchanged.</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════ SECTION 7 — IMPLEMENTATION NOTES ═ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num">7</div>
<div class="sec-meta">
<span class="sec-badge badge-impl">Implementation</span>
<div class="sec-title">Implementation notes — developer checklist</div>
<div class="sec-tagline">Numbered rules. Each rule maps to a specific line-level change in one of the three files.</div>
</div>
</div>
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;overflow:hidden">
<div class="IMPL-RULES">
<div class="IMPL-RULE">
<div class="IMPL-NUM">1</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Narrow column container (ConversationTimeline.svelte)</div>
<div class="IMPL-TEXT">Wrap the <code>flex flex-col gap-4</code> div (currently inside <code>p-6 md:p-8</code>) with <code>&lt;div class="max-w-[640px] mx-auto"&gt;</code>. The outer <code>relative overflow-hidden rounded-sm border border-line bg-surface shadow-sm</code> container stays unchanged at full page width.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">2</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Remove central vertical line (ConversationTimeline.svelte)</div>
<div class="IMPL-TEXT">Delete the entire <code>&lt;div class="absolute top-0 bottom-0 left-1/2 hidden w-px -translate-x-1/2 transform bg-muted md:block"&gt;&lt;/div&gt;</code> element. It served as a decorative lane divider on wide screens; inside a narrow column it would bisect the bubbles incorrectly.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">3</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Bubble max-width (ConversationTimeline.svelte)</div>
<div class="IMPL-TEXT">Change <code>max-w-[90%] md:max-w-[70%]</code> to <code>max-w-[80%]</code>. No breakpoint prefix is needed — the column itself is narrow so 80 % of 640 px (~512 px) is the effective max on all screens.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">4</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Status text label (ConversationTimeline.svelte — WCAG fix)</div>
<div class="IMPL-TEXT">Replace the lone <code>&lt;span class="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full … " title={doc.status}&gt;&lt;/span&gt;</code> with:<br>
<code>&lt;span class="flex items-center gap-1"&gt;&lt;span class="h-1.5 w-1.5 flex-shrink-0 rounded-full {colorClass}"&gt;&lt;/span&gt;&lt;span class="text-[9px] font-sans uppercase tracking-wider {labelColorClass}"&gt;{statusLabel}&lt;/span&gt;&lt;/span&gt;</code><br>
Define <code>const statusLabels: Record&lt;string, string&gt;</code> mapping status codes to <code>m.conv_status_*()</code> calls. Derive <code>statusLabel</code> as <code>statusLabels[doc.status] ?? doc.status</code>.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">5</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">filterExpanded state (+page.svelte)</div>
<div class="IMPL-TEXT">Add <code>let filterExpanded = $state(!data.filters.senderId || !data.filters.receiverId)</code> after the existing state declarations. Inside the existing <code>$effect</code> that syncs filter values, append: <code>if (senderId &amp;&amp; receiverId) filterExpanded = false;</code>. This auto-collapses after a successful navigation that loads results.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">6</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Pass expanded to FilterBar (+page.svelte)</div>
<div class="IMPL-TEXT">Add <code>bind:expanded={filterExpanded}</code> to the <code>&lt;ConversationFilterBar …&gt;</code> element. This is the only change to the JSX-like template in <code>+page.svelte</code> beyond step 5.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">7</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Collapsed strip rendering (ConversationFilterBar.svelte)</div>
<div class="IMPL-TEXT">Add <code>expanded = $bindable(true)</code> to the prop definition. Wrap the existing form content in <code>{#if expanded}…{/if}</code>. Add a new <code>{:else}</code> branch that renders the single-line strip: <code>&lt;div class="flex items-center justify-between px-4 py-2 border-b border-line bg-surface mb-10"&gt;</code> containing person names + "Anpassen" button. The button's <code>onclick</code> sets <code>expanded = true</code>.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">8</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Apply / Cancel in expanded overlay mode (ConversationFilterBar.svelte)</div>
<div class="IMPL-TEXT">When <code>expanded</code> is <code>true</code> and a conversation was previously active (i.e. <code>senderId &amp;&amp; receiverId</code> are non-empty at mount time), render "Anwenden" and "Abbrechen" buttons at the bottom of the form. "Anwenden" calls <code>onapplyFilters()</code> then sets <code>expanded = false</code>. "Abbrechen" only sets <code>expanded = false</code>. When no conversation is active, these extra buttons are not shown — the person typeahead's <code>onchange</code> already fires <code>onapplyFilters()</code> automatically.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">9</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Avatar visibility unchanged</div>
<div class="IMPL-TEXT">Keep <code>hidden sm:block</code> on the avatar wrapper. On mobile (375 px) avatars are hidden; on tablet+ they show at 32×32. No change needed — this class already exists in <code>ConversationTimeline.svelte</code>.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">10</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Year divider — no change</div>
<div class="IMPL-TEXT">The existing <code>data-testid="year-divider"</code> element uses <code>flex-grow border-t</code> which auto-fills its container. Moving it inside the <code>max-w-[640px]</code> wrapper is the only structural change, and that is covered by rule 1. No attribute or class change needed on the divider element itself.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">11</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">i18n keys to add</div>
<div class="IMPL-TEXT">Add 6 keys to <code>messages/de.json</code>, <code>en.json</code>, and <code>es.json</code>. See i18n table below.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">12</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">Keyboard / accessibility</div>
<div class="IMPL-TEXT">"Anpassen" is a <code>&lt;button&gt;</code> — natively focusable, responds to Enter/Space. No ARIA additions needed beyond what a standard button provides. The status text label (rule 4) resolves the only existing WCAG colour-only information issue on this page.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">13</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">canWrite guard unchanged</div>
<div class="IMPL-TEXT">The <code>{#if canWrite}</code> block around the "+ Neues Dokument" link in <code>ConversationTimeline.svelte</code> is outside the narrow column wrapper (it sits in the summary bar). No change needed.</div>
</div>
</div>
<div class="IMPL-RULE">
<div class="IMPL-NUM">14</div>
<div class="IMPL-BODY">
<div class="IMPL-TITLE">data-testid attributes preserved</div>
<div class="IMPL-TEXT">Existing test IDs must not be removed: <code>conv-swap-btn</code>, <code>conv-summary</code>, <code>conv-new-doc-link</code>, <code>year-divider</code>. The new "Anpassen" button should receive <code>data-testid="conv-filter-adjust-btn"</code>.</div>
</div>
</div>
</div>
</div>
<!-- i18n table -->
<div style="margin-top:24px;background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;overflow:hidden">
<div style="padding:12px 16px;font-size:10px;font-weight:800;color:#555;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #E0DDD6;background:#F7F5F2">i18n keys to add (messages/de.json · en.json · es.json)</div>
<table class="I18N-TABLE">
<thead><tr><th>Key</th><th>de</th><th>en</th><th>es</th></tr></thead>
<tbody>
<tr><td><code>conv_filter_adjust</code></td><td class="de">Anpassen</td><td class="en">Adjust</td><td class="es">Ajustar</td></tr>
<tr><td><code>conv_filter_apply</code></td><td class="de">Anwenden</td><td class="en">Apply</td><td class="es">Aplicar</td></tr>
<tr><td><code>conv_filter_cancel</code></td><td class="de">Abbrechen</td><td class="en">Cancel</td><td class="es">Cancelar</td></tr>
<tr><td><code>conv_status_placeholder</code></td><td class="de">Platzhalter</td><td class="en">Placeholder</td><td class="es">Marcador</td></tr>
<tr><td><code>conv_status_uploaded</code></td><td class="de">Hochgeladen</td><td class="en">Uploaded</td><td class="es">Subido</td></tr>
<tr><td><code>conv_status_transcribed</code></td><td class="de">Transkribiert</td><td class="en">Transcribed</td><td class="es">Transcrito</td></tr>
<tr><td><code>conv_status_reviewed</code></td><td class="de">Geprüft</td><td class="en">Reviewed</td><td class="es">Revisado</td></tr>
<tr><td><code>conv_status_archived</code></td><td class="de">Archiviert</td><td class="en">Archived</td><td class="es">Archivado</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ═══════════════════════════════════════ BEFORE / AFTER COMPARISON ════════ -->
<div class="section">
<div class="sec-intro">
<div class="sec-num" style="color:#C8C4BE">Δ</div>
<div class="sec-meta">
<span class="sec-badge badge-arch">Comparison</span>
<div class="sec-title">Before / after — full diff summary</div>
<div class="sec-tagline">Side-by-side summary of every meaningful behavioural and visual change.</div>
</div>
</div>
<div class="CMP">
<div class="CMP-col before">
<div class="CMP-head before">Before</div>
<div class="CMP-body">
<div class="CMP-item">Full filter form always visible, never collapses</div>
<div class="CMP-item">Bubbles race to opposite edges of 100 % screen width</div>
<div class="CMP-item">Central vertical grey line bisects wide chat container</div>
<div class="CMP-item">Bubble max-width: <code>md:max-w-[70%]</code> — up to 70 % of full page at desktop</div>
<div class="CMP-item">Status: colour-only dot, <code>title</code> tooltip (WCAG failure)</div>
<div class="CMP-item">Status dot classes: only <code>bg-accent</code> (uploaded) vs <code>bg-yellow-400</code> (all else)</div>
<div class="CMP-item">No "Anpassen" / "Anwenden" / "Abbrechen" controls</div>
<div class="CMP-item">Mobile: avatars hidden by <code>hidden sm:block</code> — correct but max-width too wide</div>
</div>
</div>
<div class="CMP-col after">
<div class="CMP-head after">After</div>
<div class="CMP-body">
<div class="CMP-item">Filter auto-collapses to single strip when conversation loads; re-expands on demand</div>
<div class="CMP-item">All bubbles centred in <code>max-w-[640px]</code> column — tight WhatsApp-style gap</div>
<div class="CMP-item">Central line removed — irrelevant inside narrow column</div>
<div class="CMP-item">Bubble max-width: <code>max-w-[80%]</code> — 80 % of 640 px = ~512 px, same visual weight across breakpoints</div>
<div class="CMP-item">Status: dot + text label side-by-side (WCAG 1.4.1 satisfied)</div>
<div class="CMP-item">Status dot has 5 distinct classes mapping to full <code>DocumentStatus</code> lifecycle</div>
<div class="CMP-item">FilterBar gains "Anpassen" (strip), "Anwenden" + "Abbrechen" (overlay)</div>
<div class="CMP-item">Mobile unchanged in avatar logic; bubble group max-width explicitly set to <code>max-w-[85%]</code></div>
</div>
</div>
</div>
</div>
</div><!-- /.doc -->
</body>
</html>