Files
familienarchiv/docs/specs/nl-search-spec.html
Marcel 62c8ce4cb2
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
docs(search): add NL search visual spec — toggle pill, chips, full-area states (#739)
Covers the SmartModeToggle pill (inside the search input, Google AI Mode
style), InterpretationChipRow anatomy, DisambiguationPicker, and all
status/error/empty states as full-result-area panels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 16:47:09 +02:00

1213 lines
74 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>NL Search — Toggle, Chips &amp; States · /documents · Familienarchiv</title>
<link href="https://fonts.googleapis.com/css2?family=Tinos:ital,wght@0,400;0,700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Montserrat',system-ui,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5;font-size:13px}
.doc{max-width:1300px;margin:0 auto;padding:48px 32px 120px}
/* ── Masthead ── */
.mh{padding-bottom:24px;border-bottom:3px solid #012851;margin-bottom:60px}
.mh h1{font-size:23px;font-weight:900;color:#012851;letter-spacing:-.4px}
.mh p{font-size:13px;color:#555;max-width:740px;line-height:1.75;margin-top:8px}
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:10px}
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.tag{background:#012851;color:#A1DCD8;padding:2px 8px;border-radius:2px;font-size:8px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}
.tag.amber{background:#7c4a00;color:#fde68a}
/* ── Section headers ── */
.sh{margin:0 0 28px}
.sh h2{font-size:16px;font-weight:900;color:#012851;letter-spacing:-.2px}
.sh p{font-size:12.5px;color:#666;max-width:720px;line-height:1.7;margin-top:5px}
.section{margin-bottom:80px;padding-bottom:80px;border-bottom:2px dashed #C8C4BE}
.section:last-of-type{border-bottom:none;margin-bottom:0;padding-bottom:0}
/* ── Token tables ── */
.token-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.token-table{border-radius:6px;overflow:hidden}
.token-table.light{background:#fff;border:1px solid #E0DDD6}
.token-table.dark{background:#0F1923;border:1px solid #1E2D3D}
.token-head{padding:8px 14px;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid #E0DDD6}
.token-table.light .token-head{background:#F4F2EC;color:#888;border-bottom-color:#E0DDD6}
.token-table.dark .token-head{background:#0A1218;color:#4E6070;border-bottom-color:#1E2D3D}
.token-table table{width:100%;border-collapse:collapse;font-size:11px}
.token-table.light td{padding:6px 14px;border-bottom:1px solid #F0EEE8;vertical-align:middle}
.token-table.dark td{padding:6px 14px;border-bottom:1px solid #1A2830;vertical-align:middle;color:#8AAABB}
.token-table tr:last-child td{border-bottom:none}
.token-table.light td:first-child{font-size:9px;font-weight:700;color:#888;width:210px}
.token-table.dark td:first-child{font-size:9px;font-weight:700;color:#4E6070;width:210px}
.swatch{display:inline-block;width:12px;height:12px;border-radius:2px;vertical-align:middle;margin-right:6px}
.warn{display:inline-block;background:#FEF3C7;color:#92400E;font-size:8px;font-weight:700;padding:1px 5px;border-radius:2px;margin-left:4px;vertical-align:middle}
.pass{display:inline-block;background:#D1FAE5;color:#065F46;font-size:8px;font-weight:700;padding:1px 5px;border-radius:2px;margin-left:4px;vertical-align:middle}
/* ── Browser chrome ── */
.chrome-bar{height:20px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 8px;gap:4px}
.chrome-bar.dk{background:#010a18;border-bottom-color:#0d3358}
.chrome-dot{width:6px;height:6px;border-radius:50%;background:#BDB8B1}
.chrome-dot.dk{background:#1a2a3a}
.chrome-url{flex:1;height:9px;background:#CCC8C2;border-radius:5px;margin-left:6px}
.chrome-url.dk{background:#1a2a3a}
/* ── App nav ── */
.app-nav{height:30px;background:#012851;display:flex;align-items:center;padding:0 12px;gap:10px;flex-shrink:0}
.app-logo{font-family:'Tinos',Georgia,serif;font-size:7px;font-weight:700;color:#fff;border-bottom:2px solid #A1DCD8;padding-bottom:1px}
.app-link{font-size:5.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:rgba(255,255,255,.4);white-space:nowrap}
.app-link.on{color:rgba(255,255,255,.9)}
.app-nav-r{margin-left:auto;display:flex;gap:6px;align-items:center}
.app-av{width:16px;height:16px;border-radius:50%;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:rgba(255,255,255,.5)}
/* ── Scale wrappers ── */
.scale-outer{border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)}
.scale-outer.dk{border-color:#0d3358}
.scale-inner{width:1280px;transform:scale(0.65);transform-origin:top left}
.scale-outer-sm{border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)}
.scale-inner-sm{width:520px;transform:scale(0.82);transform-origin:top left}
/* ── Split layouts ── */
.split2{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:16px}
.split3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin-bottom:16px}
.screen-lbl{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:8px;display:flex;align-items:center;gap:5px}
.lbl-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
.cap{font-size:10px;color:#999;font-style:italic;line-height:1.6;margin-top:10px;max-width:520px}
/* ── Rules table ── */
.rules{background:#fff;border:1px solid #E0DDD6;border-radius:6px;overflow:hidden}
.rules table{width:100%;border-collapse:collapse}
.rules th{background:#F4F2EC;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;padding:8px 12px;text-align:left;border-bottom:1px solid #E0DDD6}
.rules td{font-size:11px;color:#444;padding:8px 12px;border-bottom:1px solid #F0EEE8;vertical-align:top;line-height:1.6}
.rules tr:last-child td{border-bottom:none}
.rules td:first-child{font-size:9px;font-weight:700;color:#012851;white-space:nowrap;width:190px}
.rules td code{font-size:9px;background:#F0EFE9;padding:1px 4px;border-radius:2px;color:#555;white-space:nowrap}
/* ── Annotation ── */
.ann{display:inline-block;background:#FEF3C7;color:#92400E;font-size:8px;font-weight:700;padding:2px 7px;border-radius:2px;border:1px dashed #F59E0B;margin:3px 2px}
/* ════════════════════════════════════════════════════════
SEARCH PAGE — component styles used in all mockups
════════════════════════════════════════════════════════ */
/* Page canvas */
.pg{background:#f0efe9;padding:18px 24px 24px}
.pg.dk{background:#010e1e}
.pg-h1{font-family:'Tinos',Georgia,serif;font-size:17px;font-weight:700;color:#012851;margin-bottom:2px}
.pg.dk .pg-h1{color:#f0efe9}
.pg-sub{font-size:8px;color:#9ca3af;margin-bottom:12px}
/* Filter card — mirrors real: rounded-sm border border-line bg-surface p-6 shadow-sm mb-8 */
.sfb{background:#fff;border:1.5px solid #e4e2d7;border-radius:4px;padding:14px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.06)}
.sfb.dk{background:#011526;border-color:#0d3358}
/* ── ROW 1: input + inline toggle + sort + filter + reset ── */
.sfb-r1{display:flex;align-items:center;gap:8px;margin-bottom:10px}
/* Input wrapper — position:relative holds the inline toggle pill */
.sfb-input-wrap{position:relative;flex:1}
.sfb-input{width:100%;height:38px;border:1.5px solid #e4e2d7;border-radius:3px;background:#fff;display:flex;align-items:center;padding-left:10px;padding-right:70px;font-size:10px;color:#9ca3af;font-family:'Montserrat',sans-serif}
.sfb-input.typed{color:#012851;font-family:'Tinos',Georgia,serif;font-size:10px;font-style:italic}
.sfb-input.dk{background:#010e1e;border-color:#1e3a55;color:#4e6070}
.sfb-input.typed.dk{color:#f0efe9}
/* Toggle PILL — sits inside the input, right edge, like Google's AI Mode */
.sfb-pill-wrap{position:absolute;right:6px;top:50%;transform:translateY(-50%);display:flex;align-items:center}
/* Keyword pill (resting) — subtle, muted */
.sfb-pill-kw{display:flex;align-items:center;gap:3px;border:1px solid #c8c4be;border-radius:99px;background:#f4f2ec;color:#4b5563;font-size:7.5px;font-weight:700;padding:3px 8px;cursor:default;white-space:nowrap;letter-spacing:.3px}
.sfb-pill-kw.dk{border-color:#1e3a55;background:#011526;color:#6b7280}
/* Smart pill (active) — navy fill, mint text, matches bg-primary/text-primary-fg */
.sfb-pill-smart{display:flex;align-items:center;gap:3px;border:1px solid #012851;border-radius:99px;background:#012851;color:#a1dcd8;font-size:7.5px;font-weight:700;padding:3px 8px;cursor:default;white-space:nowrap;letter-spacing:.3px}
.sfb-pill-smart.dk{border-color:#a1dcd8;background:#a1dcd8;color:#012851}
/* Sort + Filter + Reset buttons */
.sfb-sort-btn{height:38px;border:1.5px solid #e4e2d7;border-radius:3px;font-size:8px;font-weight:700;color:#012851;padding:0 8px;display:flex;align-items:center;gap:4px;cursor:default;background:#fff;text-transform:uppercase;letter-spacing:.4px;white-space:nowrap}
.sfb-sort-btn.dk{border-color:#1e3a55;background:#011526;color:#8b97a5}
.sfb-filter-btn{height:38px;border:1.5px solid #e4e2d7;border-radius:3px;font-size:8px;font-weight:700;color:#012851;padding:0 8px;display:flex;align-items:center;gap:4px;cursor:default;background:#f4f2ec;text-transform:uppercase;letter-spacing:.4px}
.sfb-filter-btn.dk{border-color:#1e3a55;background:#011526;color:#8b97a5}
.sfb-reset-btn{height:38px;width:38px;display:flex;align-items:center;justify-content:center;border:1.5px solid transparent;cursor:default;color:#c8c4be}
/* ── ROW 2: advanced filters (collapsed placeholder) ── */
.sfb-advanced-hint{font-size:8px;color:#c8c4be;padding:4px 2px 0;letter-spacing:.3px}
/* ── Chip row (below filter card) ── */
.chip-row{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
/* Standard filter chip */
.chip{display:inline-flex;align-items:stretch;border:1.5px solid #012851;border-radius:99px;background:#fff;overflow:hidden;cursor:default}
.chip.dk{border-color:#8b97a5;background:#011526}
.chip-body{display:flex;align-items:center;gap:3px;padding:4px 7px 4px 10px}
.chip-pfx{font-size:8px;font-weight:700;color:#012851;opacity:.65}
.chip.dk .chip-pfx{color:#9ca3af;opacity:1}
.chip-nm{font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851}
.chip.dk .chip-nm{color:#f0efe9}
.chip-arr{font-size:8px;color:#6b7280;padding:0 1px}
.chip-x{width:26px;display:flex;align-items:center;justify-content:center;border-left:1px solid rgba(1,40,81,.15);color:#012851;font-size:10px}
.chip.dk .chip-x{border-left-color:rgba(139,151,165,.2);color:#9ca3af}
/* Disambiguation chip — amber */
.chip.ambig{border-color:#d97706;background:#fffbeb}
.chip.ambig .chip-pfx{color:#92400e;opacity:1}
.chip.ambig .chip-nm{color:#78350f}
.chip.ambig .chip-x{border-left-color:rgba(217,119,6,.2);color:#d97706}
.chip-hint{font-size:7.5px;color:#92400e;opacity:.75;padding:4px 8px 4px 3px;font-style:italic;display:flex;align-items:center;white-space:nowrap}
/* ── STATUS STATES — loading / error / empty
These sit in the results area; must fill the space. ── */
.status-area{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 24px;gap:10px;text-align:center;min-height:160px}
/* Loading */
.status-spinner-ring{width:36px;height:36px;border:3px solid rgba(1,40,81,.12);border-top-color:#012851;border-radius:50%}
.dk .status-spinner-ring{border-color:rgba(161,220,216,.12);border-top-color:#a1dcd8}
.status-loading-title{font-size:12px;font-weight:700;color:#012851;letter-spacing:-.1px}
.dk .status-loading-title{color:#a1dcd8}
.status-loading-sub{font-size:9px;color:#9ca3af;max-width:280px;line-height:1.6}
/* Error */
.status-icon-circle{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:18px}
.status-icon-circle.err{background:#fef2f2;border:2px solid #f87171}
.status-icon-circle.warn{background:#fffbeb;border:2px solid #fbbf24}
.status-error-title{font-size:12px;font-weight:700;color:#1f2937;letter-spacing:-.1px}
.dk .status-error-title{color:#f0efe9}
.status-error-body{font-size:9px;color:#6b7280;max-width:300px;line-height:1.7}
.dk .status-error-body{color:#8b97a5}
.status-err-btn{margin-top:4px;display:inline-flex;align-items:center;height:32px;padding:0 14px;border:1.5px solid #012851;border-radius:3px;font-size:9px;font-weight:700;color:#012851;background:#fff;cursor:default}
.dk .status-err-btn{border-color:#a1dcd8;color:#a1dcd8;background:transparent}
/* Empty */
.status-empty-icon{width:44px;height:44px;opacity:.25}
.status-empty-title{font-size:12px;font-weight:700;color:#1f2937;letter-spacing:-.1px}
.dk .status-empty-title{color:#f0efe9}
.status-empty-body{font-size:9px;color:#6b7280;max-width:300px;line-height:1.7}
.dk .status-empty-body{color:#8b97a5}
.status-empty-link{margin-top:4px;display:inline;color:#012851;font-weight:700;font-size:9px;text-decoration:underline;cursor:default}
.dk .status-empty-link{color:#a1dcd8}
/* ── Doc result rows ── */
.doc-list{display:flex;flex-direction:column;gap:5px}
.doc-row{background:#fff;border:1.5px solid #e4e2d7;border-radius:3px;padding:8px 10px;display:flex;gap:8px;align-items:flex-start}
.doc-row.dk{background:#011526;border-color:#0d3358}
.doc-thumb{width:22px;height:28px;background:#f0efe9;border:1px solid #e4e2d7;border-radius:1px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:6px;color:#c8c4be}
.doc-row.dk .doc-thumb{background:#010e1e;border-color:#1e3a55;color:#1e3a55}
.doc-info{flex:1}
.doc-title{font-family:'Tinos',Georgia,serif;font-size:9px;font-weight:700;color:#012851;margin-bottom:2px}
.doc-row.dk .doc-title{color:#f0efe9}
.doc-meta{font-size:7px;color:#9ca3af}
/* ── Picker disclosure ── */
.picker-panel{background:#fff;border:1.5px solid #012851;border-radius:4px;padding:7px;margin-top:3px;min-width:260px}
.picker-panel.dk{background:#011526;border-color:#a1dcd8}
.picker-lbl{font-size:7.5px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#9ca3af;display:block;padding:2px 4px 5px}
.picker-item{display:flex;align-items:center;gap:6px;padding:5px 6px;border-radius:3px;cursor:default}
.picker-item.hi{background:rgba(1,40,81,.07)}
.picker-panel.dk .picker-item.hi{background:rgba(161,220,216,.1)}
.picker-cb{width:11px;height:11px;border:1.5px solid #012851;border-radius:2px;flex-shrink:0}
.picker-panel.dk .picker-cb{border-color:#a1dcd8}
.picker-cb.on{background:#012851;border-color:#012851;display:flex;align-items:center;justify-content:center;color:#fff;font-size:7px}
.picker-panel.dk .picker-cb.on{background:#a1dcd8;border-color:#a1dcd8;color:#012851}
.picker-nm{font-family:'Tinos',Georgia,serif;font-size:9px;color:#012851}
.picker-panel.dk .picker-nm{color:#f0efe9}
.picker-yr{font-size:7.5px;color:#9ca3af;margin-left:3px}
.picker-footer{display:flex;gap:5px;justify-content:flex-end;border-top:1px solid #f0efe9;margin-top:5px;padding-top:6px}
.picker-panel.dk .picker-footer{border-top-color:#1e3a55}
.picker-cancel-btn{height:22px;padding:0 8px;border:1.5px solid #e4e2d7;border-radius:3px;font-size:7.5px;font-weight:700;color:#6b7280;background:#fff;cursor:default}
.picker-panel.dk .picker-cancel-btn{border-color:#1e3a55;background:#011526;color:#8b97a5}
.picker-ok-btn{height:22px;padding:0 8px;border-radius:3px;font-size:7.5px;font-weight:700;color:#a1dcd8;background:#012851;cursor:default}
.picker-panel.dk .picker-ok-btn{background:#a1dcd8;color:#012851}
</style>
</head>
<body>
<div class="doc">
<!-- ══ MASTHEAD ══════════════════════════════════════════════════════════════ -->
<div class="mh">
<h1>NL Search — Toggle, Interpretation Chips &amp; States · /documents</h1>
<p>
Visual specification for the natural-language search mode. The mode toggle lives
<strong>inside the search input as an inline pill</strong> — inspired by Google's AI Mode button —
keeping the filter row unchanged. Loading, error, and empty states are full-height panels
that fill the result area rather than inline one-liners.
Issue #739 — Archive Intelligence milestone.
</p>
<div class="byline">Familienarchiv · 2026-06-06 · Leonie Voss, UX Lead</div>
<div class="tag-row">
<span class="tag">NL Search · Issue #739</span>
<span class="tag">Desktop 1280 px / Mobile 320 px</span>
<span class="tag">Light + Dark</span>
<span class="tag">Archive Intelligence Milestone</span>
</div>
</div>
<!-- ══ SECTION 1 — DESIGN TOKENS ════════════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>1 · Design tokens</h2>
<p>All colour values for the toggle pill, chips, and status states. No new tokens — everything maps to existing <code>layout.css</code> semantics.</p>
</div>
<div class="token-grid">
<div class="token-table light">
<div class="token-head">Light theme</div>
<table>
<tr>
<td>Toggle pill — keyword (resting)</td>
<td><span class="swatch" style="background:#f4f2ec;border:1px solid #c8c4be"></span>#f4f2ec bg · #c8c4be border · #4b5563 text — muted, doesn't compete with query text</td>
</tr>
<tr>
<td>Toggle pill — smart (active)</td>
<td><span class="swatch" style="background:#012851"></span>#012851 bg (bg-primary) · <span class="swatch" style="background:#a1dcd8;border:1px solid #ccc"></span>#a1dcd8 text (text-primary-fg)<span class="pass">9.2:1 AAA ✓</span></td>
</tr>
<tr>
<td>Chip border / text</td>
<td><span class="swatch" style="background:#012851"></span>#012851 — navy 1.5 px; Tinos for person names<span class="pass">14.5:1 AAA ✓</span></td>
</tr>
<tr>
<td>Chip prefix (type label)</td>
<td>#012851 at 65% opacity — "Absender:", "Zeitraum:", "Stichwort:"</td>
</tr>
<tr>
<td>Chip × divider</td>
<td>rgba(1,40,81,.15) — 1 px left border inside chip</td>
</tr>
<tr>
<td>Disambiguation chip border</td>
<td><span class="swatch" style="background:#d97706"></span>#d97706 — amber; signals disambiguation needed</td>
</tr>
<tr>
<td>Status area — loading spinner</td>
<td>rgba(1,40,81,.12) track + #012851 active slice; 36 px dia, 3 px stroke</td>
</tr>
<tr>
<td>Status area — error icon circle</td>
<td>#fef2f2 bg · #f87171 border — red-100/red-400</td>
</tr>
<tr>
<td>Status area — warning icon circle</td>
<td>#fffbeb bg · #fbbf24 border — amber-50/amber-400</td>
</tr>
<tr>
<td>Focus ring (chips + link)</td>
<td>focus-visible:ring-2 focus-visible:ring-brand-navy on chip wrappers and fallback link</td>
</tr>
</table>
</div>
<div class="token-table dark">
<div class="token-head">Dark theme</div>
<table>
<tr>
<td>Toggle pill — keyword (resting)</td>
<td><span class="swatch" style="background:#011526;border:1px solid #1e3a55"></span>#011526 bg · #1e3a55 border · #6b7280 text</td>
</tr>
<tr>
<td>Toggle pill — smart (active)</td>
<td><span class="swatch" style="background:#a1dcd8;border:1px solid #ccc"></span>#a1dcd8 bg · #012851 text — exact inverse<span class="pass" style="background:rgba(209,250,229,.15);color:#6EE7B7">9.2:1 AAA ✓</span></td>
</tr>
<tr>
<td>Chip border</td>
<td><span class="swatch" style="background:#8b97a5;border:1px solid #444"></span>#8b97a5 — blue-grey</td>
</tr>
<tr>
<td>Chip name text</td>
<td><span class="swatch" style="background:#f0efe9;border:1px solid #555"></span>#f0efe9 — sand-white<span class="pass" style="background:rgba(209,250,229,.15);color:#6EE7B7">14.5:1 AAA ✓</span></td>
</tr>
<tr>
<td>Status area — loading spinner</td>
<td>rgba(161,220,216,.12) track + #a1dcd8 mint active slice</td>
</tr>
<tr>
<td>Status loading title</td>
<td><span class="swatch" style="background:#a1dcd8;border:1px solid #555"></span>#a1dcd8 — mint</td>
</tr>
<tr>
<td>Status error title</td>
<td><span class="swatch" style="background:#f0efe9;border:1px solid #555"></span>#f0efe9 — sand-white</td>
</tr>
<tr>
<td>Status body text</td>
<td><span class="swatch" style="background:#8b97a5;border:1px solid #444"></span>#8b97a5 — muted blue-grey</td>
</tr>
<tr>
<td>Error action button</td>
<td>border + text #a1dcd8 mint on transparent bg</td>
</tr>
<tr>
<td>Empty state fallback link</td>
<td>#a1dcd8 mint, underlined</td>
</tr>
</table>
</div>
</div>
</div>
<!-- ══ SECTION 2 — TOGGLE PILL INSIDE INPUT ══════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>2 · Toggle pill inside the search input — keyword vs smart mode</h2>
<p>The toggle is an absolutely-positioned pill at the <strong>right edge of the search input</strong>, replacing the magnifier icon. The input gets extra right padding (<code>pr-28</code>) to keep query text from overlapping the pill. The rest of the filter row — Sort, Filter, Reset — is unchanged. On desktop the pill reads "Text" or "KI"; on mobile (below <code>sm</code>) it reads "KI-Suche" / "Textsuche" for clarity.</p>
</div>
<!-- ── Toggle zoom: anatomy ── -->
<div style="margin-bottom:32px">
<div class="screen-lbl"><span class="lbl-dot" style="background:#6b7280"></span>Toggle pill — anatomy (both states at 2× zoom)</div>
<div style="display:flex;gap:20px;align-items:flex-end;flex-wrap:wrap;margin-bottom:10px">
<!-- Keyword pill -->
<div style="background:#f0efe9;padding:18px 20px;border-radius:6px;border:1.5px solid #e4e2d7">
<div style="font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#9ca3af;margin-bottom:12px">Keyword mode (resting)</div>
<!-- 2x enlarged pill -->
<div style="display:flex;align-items:center;gap:6px;border:2px solid #c8c4be;border-radius:99px;background:#f4f2ec;color:#4b5563;font-size:14px;font-weight:700;padding:6px 16px;width:fit-content">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="3" width="14" height="10" rx="2"/><line x1="4" y1="7" x2="12" y2="7"/><line x1="4" y1="10" x2="9" y2="10"/></svg>
Text
</div>
<div style="margin-top:10px;font-size:9px;color:#9ca3af;line-height:1.6">
bg: #f4f2ec (bg-muted)<br>border: #c8c4be (border-line)<br>text: #4b5563 (text-ink-2)<br>aria-pressed="false"
</div>
</div>
<!-- Smart pill -->
<div style="background:#f0efe9;padding:18px 20px;border-radius:6px;border:1.5px solid #e4e2d7">
<div style="font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#9ca3af;margin-bottom:12px">Smart mode (active)</div>
<div style="display:flex;align-items:center;gap:6px;border:2px solid #012851;border-radius:99px;background:#012851;color:#a1dcd8;font-size:14px;font-weight:700;padding:6px 16px;width:fit-content">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
<div style="margin-top:10px;font-size:9px;color:#9ca3af;line-height:1.6">
bg: #012851 (bg-primary)<br>text: #a1dcd8 (text-primary-fg)<br>Matches AND/OR operator active<br>aria-pressed="true"
</div>
</div>
<!-- Smart pill dark -->
<div style="background:#010e1e;padding:18px 20px;border-radius:6px;border:1.5px solid #0d3358">
<div style="font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:#4e6070;margin-bottom:12px">Smart mode — dark</div>
<div style="display:flex;align-items:center;gap:6px;border:2px solid #a1dcd8;border-radius:99px;background:#a1dcd8;color:#012851;font-size:14px;font-weight:700;padding:6px 16px;width:fit-content">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
<div style="margin-top:10px;font-size:9px;color:#4e6070;line-height:1.6">
bg: #a1dcd8 (mint) — inverted<br>text: #012851 (navy)<br>Same pattern as selected node dark
</div>
</div>
</div>
</div>
<!-- ── Full search bar — side by side ── -->
<div class="split2">
<!-- Keyword mode light -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#A1DCD8;border:1px solid #ccc"></span>Full filter bar — keyword mode (light)</div>
<div class="scale-outer" style="width:832px;height:84px">
<div class="scale-inner" style="height:129px">
<div class="pg" style="padding:14px 24px">
<div class="sfb" style="padding:12px;margin-bottom:0">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input">Suche nach Absender, Empfänger, Inhalt…</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-kw">
<svg width="9" height="9" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="1" y="3" width="14" height="10" rx="2"/><line x1="4" y1="7" x2="12" y2="7"/><line x1="4" y1="10" x2="9" y2="10"/></svg>
Text
</div>
</div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg>
Filter
</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
</div>
</div>
</div>
<p class="cap">Keyword mode. The "Text" pill sits at the right edge of the input, muted (#f4f2ec) so it doesn't compete with the query text. Sort, Filter, and Reset remain in the same position in the filter row.</p>
</div>
<!-- Smart mode light -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#012851"></span>Full filter bar — smart mode active (light)</div>
<div class="scale-outer" style="width:832px;height:84px">
<div class="scale-inner" style="height:129px">
<div class="pg" style="padding:14px 24px">
<div class="sfb" style="padding:12px;margin-bottom:0">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Was hat walter im krieg geschrieben?</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart">
<svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
</div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg>
Filter
</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
</div>
</div>
</div>
<p class="cap">Smart mode active. The "KI" pill becomes navy (#012851) with mint text (#a1dcd8) — bg-primary / text-primary-fg, matching the AND/OR operator active state. Query text is in Tinos italic. The rest of the bar is unchanged.</p>
</div>
</div>
<!-- dark variants -->
<div class="split2" style="margin-top:0">
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#1a2a3a"></span>Keyword mode — dark</div>
<div class="scale-outer dk" style="width:832px;height:84px">
<div class="scale-inner" style="height:129px;background:#010e1e">
<div class="pg dk" style="padding:14px 24px">
<div class="sfb dk" style="padding:12px;margin-bottom:0">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input dk">Suche nach Absender, Empfänger, Inhalt…</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-kw dk">
<svg width="9" height="9" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="1" y="3" width="14" height="10" rx="2"/><line x1="4" y1="7" x2="12" y2="7"/><line x1="4" y1="10" x2="9" y2="10"/></svg>
Text
</div>
</div>
</div>
<div class="sfb-sort-btn dk">Sortierung ▾</div>
<div class="sfb-filter-btn dk">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg>
Filter
</div>
<div class="sfb-reset-btn" style="color:#2a3a4a"></div>
</div>
</div>
</div>
</div>
</div>
<p class="cap">Dark, keyword mode. Pill uses #1e3a55 border on #011526 bg with #6b7280 text.</p>
</div>
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#a1dcd8;border:1px solid #555"></span>Smart mode — dark</div>
<div class="scale-outer dk" style="width:832px;height:84px">
<div class="scale-inner" style="height:129px;background:#010e1e">
<div class="pg dk" style="padding:14px 24px">
<div class="sfb dk" style="padding:12px;margin-bottom:0">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed dk">Was hat walter im krieg geschrieben?</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart dk">
<svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
</div>
</div>
<div class="sfb-sort-btn dk">Sortierung ▾</div>
<div class="sfb-filter-btn dk">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg>
Filter
</div>
<div class="sfb-reset-btn" style="color:#2a3a4a"></div>
</div>
</div>
</div>
</div>
</div>
<p class="cap">Dark, smart mode. Pill inverts to mint bg (#a1dcd8) + navy text (#012851).</p>
</div>
</div>
</div>
<!-- ══ SECTION 3 — LOADING STATE ═════════════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>3 · Loading state — full-area panel</h2>
<p>The loading state fills the result area instead of showing a tiny inline spinner. A centred, vertically-padded panel with a large spinner ring and two lines of text matches the space that results would normally occupy. <code>role="status" aria-live="polite"</code> announces arrival to screen readers without stealing focus. Spinner uses <code>motion-safe:animate-spin</code>, subtitle uses <code>motion-safe:animate-pulse</code>.</p>
</div>
<div class="split2">
<!-- Light loading -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#A1DCD8;border:1px solid #ccc"></span>Light — loading</div>
<div class="scale-outer" style="width:832px;height:325px">
<div class="scale-inner" style="height:500px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-link on">Dokumente</div>
<div class="app-link">Personen</div>
<div class="app-link">Stammbaum</div>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">Intelligente Suche aktiv</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Was hat walter im krieg geschrieben?</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart">
<svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
</div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<!-- Loading panel -->
<div class="status-area" role="status" aria-live="polite">
<div class="status-spinner-ring"></div>
<div class="status-loading-title">Archiv wird befragt…</div>
<div class="status-loading-sub">Die KI analysiert Ihre Anfrage.<br>Das kann bis zu 15 Sekunden dauern.</div>
</div>
<div style="text-align:center">
<span class="ann">role="status" aria-live="polite"</span>
<span class="ann">motion-safe:animate-spin (ring)</span>
<span class="ann">motion-safe:animate-pulse (subtitle)</span>
</div>
</div>
</div>
</div>
<p class="cap">Light. The spinner ring is 36 px with 3 px stroke. Subtitle explains the expected wait — "bis zu 15 Sekunden" manages expectations. No timeout countdown shown during the request.</p>
</div>
<!-- Dark loading -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#1a2a3a"></span>Dark — loading</div>
<div class="scale-outer dk" style="width:832px;height:325px">
<div class="scale-inner" style="height:500px;background:#010e1e">
<div class="chrome-bar dk"><div class="chrome-dot dk"></div><div class="chrome-dot dk"></div><div class="chrome-dot dk"></div><div class="chrome-url dk"></div></div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-link on">Dokumente</div>
<div class="app-link">Personen</div>
<div class="app-link">Stammbaum</div>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="pg dk">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">Intelligente Suche aktiv</div>
<div class="sfb dk">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed dk">Was hat walter im krieg geschrieben?</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart dk">
<svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI
</div>
</div>
</div>
<div class="sfb-sort-btn dk">Sortierung ▾</div>
<div class="sfb-filter-btn dk"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn" style="color:#2a3a4a"></div>
</div>
</div>
<div class="status-area dk" role="status">
<div class="status-spinner-ring"></div>
<div class="status-loading-title">Archiv wird befragt…</div>
<div class="status-loading-sub" style="color:#4e6070">Die KI analysiert Ihre Anfrage.<br>Das kann bis zu 15 Sekunden dauern.</div>
</div>
</div>
</div>
</div>
<p class="cap">Dark. Spinner track rgba(161,220,216,.12), active slice mint #a1dcd8. Title in mint. Subtitle in muted #4e6070.</p>
</div>
</div>
</div>
<!-- ══ SECTION 4 — CHIPS: SINGLE-NAME ═══════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>4 · Interpretation chips — single-name query (light)</h2>
<p>After a successful NL response, <code>InterpretationChipRow</code> renders above the result list. Every chip has a type prefix so the senior audience knows what the filter is — "19141918" without "Zeitraum:" is ambiguous. Keyword chips only appear when <code>keywordsApplied === true</code>.</p>
</div>
<div class="scale-outer" style="width:832px;height:370px">
<div class="scale-inner" style="height:569px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-link on">Dokumente</div>
<div class="app-link">Personen</div>
<div class="app-link">Stammbaum</div>
<div class="app-nav-r"><div class="app-av">M</div></div>
</div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">12 Ergebnisse</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Was hat walter im krieg geschrieben?</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart"><svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg> KI</div>
</div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<!-- Chips -->
<div class="chip-row">
<div class="chip">
<div class="chip-body"><span class="chip-pfx">Absender:</span><span class="chip-nm">Walter Raddatz</span></div>
<div class="chip-x">×</div>
</div>
<div class="chip">
<div class="chip-body"><span class="chip-pfx">Zeitraum:</span><span class="chip-nm">19141918</span></div>
<div class="chip-x">×</div>
</div>
<div class="chip">
<div class="chip-body"><span class="chip-pfx">Stichwort:</span><span class="chip-nm">krieg</span></div>
<div class="chip-x">×</div>
</div>
</div>
<!-- Results -->
<div class="doc-list">
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Brief an Emma Raddatz, 14. August 1916</div><div class="doc-meta">Von Walter Raddatz · 1916 · 3 Seiten</div></div></div>
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Feldpostkarte aus Verdun, Juni 1917</div><div class="doc-meta">Von Walter Raddatz · 1917 · 1 Seite</div></div></div>
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Brief an die Familie, Weihnachten 1914</div><div class="doc-meta">Von Walter Raddatz · 1914 · 2 Seiten</div></div></div>
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Postkarte an Frieda, März 1918</div><div class="doc-meta">Von Walter Raddatz · 1918 · 1 Seite</div></div></div>
</div>
</div>
</div>
</div>
<p class="cap">Three chips: Absender (person), Zeitraum (date range), Stichwort (keyword — only because <code>keywordsApplied === true</code>). Removing any chip calls <code>GET /api/documents/search</code> with that param absent. The × button has <code>aria-label="Filter entfernen: Absender Walter Raddatz"</code>.</p>
</div>
<!-- ══ SECTION 5 — CHIPS: 2-NAME DIRECTIONAL ════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>5 · Interpretation chips — 2-name directional + long-name truncation</h2>
<p>When <code>resolvedPersons</code> has 2 entries, a single directional chip replaces two person chips. Index 0 = sender, index 1 = receiver. Each name is in a separately-truncatable <code>&lt;span&gt;</code>; the → arrow is <code>aria-hidden</code>; the chip wrapper carries a plain-language <code>aria-label</code>. The × button sits outside both spans — never clipped.</p>
</div>
<div class="split2">
<!-- Directional chip desktop -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#A1DCD8;border:1px solid #ccc"></span>Directional chip — desktop</div>
<div class="scale-outer" style="width:832px;height:240px">
<div class="scale-inner" style="height:369px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav"><div class="app-logo">Familienarchiv</div><div class="app-link on">Dokumente</div><div class="app-link">Personen</div><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">4 Ergebnisse</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Briefe von walter an emma um 1916</div>
<div class="sfb-pill-wrap"><div class="sfb-pill-smart"><svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg> KI</div></div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<div class="chip-row">
<!-- Single directional chip -->
<div class="chip">
<div class="chip-body" style="gap:2px">
<span style="max-width:128px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851">Walter Raddatz</span>
<span class="chip-arr" aria-hidden="true"></span>
<span style="max-width:128px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851">Emma Raddatz</span>
</div>
<div class="chip-x">×</div>
</div>
<div class="chip">
<div class="chip-body"><span class="chip-pfx">Zeitraum:</span><span class="chip-nm">19141918</span></div>
<div class="chip-x">×</div>
</div>
</div>
<div class="ann">aria-label="Von Walter Raddatz zu Emma Raddatz, Filter entfernen"</div>
<div class="ann">Removing clears BOTH senderId AND receiverId</div>
<div class="doc-list" style="margin-top:8px">
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Brief an Emma Raddatz, 14. August 1916</div><div class="doc-meta">Von Walter Raddatz · 1916</div></div></div>
<div class="doc-row"><div class="doc-thumb"></div><div class="doc-info"><div class="doc-title">Brief an Emma, Ostern 1915</div><div class="doc-meta">Von Walter Raddatz · 1915</div></div></div>
</div>
</div>
</div>
</div>
<p class="cap">Single directional chip for 2-name resolution. The → arrow is <code>aria-hidden</code>. Chip's <code>aria-label</code> reads "Von Walter Raddatz zu Emma Raddatz, Filter entfernen" — plain language for screen readers and the 60+ audience.</p>
</div>
<!-- Long-name truncation -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#6b7280"></span>Long-name truncation at 320 px</div>
<div class="scale-outer-sm" style="width:263px;height:93px">
<div class="scale-inner-sm" style="height:113px">
<div class="pg" style="padding:12px;min-height:unset">
<div class="chip-row" style="max-width:295px">
<div class="chip" style="max-width:100%">
<div class="chip-body" style="gap:2px;overflow:hidden;min-width:0">
<span style="max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851">Wilhelmine-Frieder…</span>
<span class="chip-arr" aria-hidden="true"></span>
<span style="max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851">Emma-Karl…</span>
</div>
<div class="chip-x" style="flex-shrink:0">×</div>
</div>
</div>
<div class="ann">max-w-[8rem] per name span; × outside both</div>
</div>
</div>
</div>
<p class="cap">At 320 px, each name span caps at <code>max-w-[8rem]</code> (128 px) with truncation ellipsis. The × button is a fixed-width sibling outside the chip-body — never clipped regardless of how long the names are.</p>
</div>
</div>
</div>
<!-- ══ SECTION 6 — DISAMBIGUATION ═══════════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>6 · Disambiguation chip &amp; picker</h2>
<p>When <code>ambiguousPersons</code> is non-empty the chip renders in amber. Clicking it opens an accessible disclosure. Focus moves into the list on open; Escape returns focus to the trigger. <strong>Build this after the multi-OR approach is confirmed in #738.</strong></p>
</div>
<div class="split2">
<!-- Ambiguous chip collapsed -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#d97706"></span>Disambiguation chip — collapsed</div>
<div class="scale-outer-sm" style="width:427px;height:82px">
<div class="scale-inner-sm" style="height:100px">
<div class="pg" style="padding:12px;min-height:unset">
<div class="chip-row" style="margin-bottom:4px">
<div class="chip ambig">
<div class="chip-body"><span class="chip-pfx">Absender:</span><span class="chip-nm">Walter Raddatz, Walter Müller</span></div>
<div class="chip-hint">(auswählen…)</div>
<div class="chip-x">×</div>
</div>
</div>
<div class="ann">aria-expanded="false" · aria-label="Mehrere Personen gefunden — zum Auswählen klicken"</div>
</div>
</div>
</div>
<p class="cap">Amber chip signals "action needed". Names listed comma-separated; "(auswählen…)" in italics makes the affordance explicit for the 60+ audience. "▾" alone is not sufficient. Search results are empty while ambiguous.</p>
</div>
<!-- Picker open -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#012851"></span>Picker open — first item focused</div>
<div class="scale-outer-sm" style="width:427px;height:221px">
<div class="scale-inner-sm" style="height:270px">
<div class="pg" style="padding:12px;min-height:unset">
<div class="chip-row" style="margin-bottom:4px">
<div class="chip ambig">
<div class="chip-body"><span class="chip-pfx">Absender:</span><span class="chip-nm">Walter Raddatz, Walter Müller</span></div>
<div class="chip-hint">(auswählen…)</div>
<div class="chip-x">×</div>
</div>
</div>
<div class="ann" style="margin-bottom:6px">aria-expanded="true" · Focus moves to first item on open</div>
<div class="picker-panel" style="max-width:320px">
<span class="picker-lbl">Welcher Walter ist gemeint?</span>
<div class="picker-item hi">
<div class="picker-cb on"></div>
<div><span class="picker-nm">Walter Raddatz</span><span class="picker-yr">18881952 · Sohn von Karl Raddatz</span></div>
</div>
<div class="picker-item">
<div class="picker-cb"></div>
<div><span class="picker-nm">Walter Müller</span><span class="picker-yr">18821941 · Ehemann von Frieda Raddatz</span></div>
</div>
<div class="picker-footer">
<div class="picker-cancel-btn">Abbrechen</div>
<div class="picker-ok-btn">Suchen</div>
</div>
</div>
<div class="ann">Escape → focus returns to trigger · "Suchen" → GET /api/documents/search</div>
</div>
</div>
</div>
<p class="cap">Picker open. Focus lands on the first item. Abbrechen (or Escape) closes and returns focus to the trigger — keyboard position never lost. Suchen fires <code>GET /api/documents/search</code> immediately with selected person IDs.</p>
</div>
</div>
</div>
<!-- ══ SECTION 7 — EMPTY &amp; ERROR STATES ═════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>7 · Empty state &amp; error states — full result-area panels</h2>
<p>All three outcome states fill the result area with a centred panel that uses the available vertical space. They share the same layout: icon → title → body → action. The height matches what a short result list would occupy so the page doesn't collapse to a one-liner with a wall of white below.</p>
</div>
<div class="split3">
<!-- Empty state -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#9ca3af"></span>Empty — zero results</div>
<div class="scale-outer" style="width:832px;height:390px">
<div class="scale-inner" style="height:600px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav"><div class="app-logo">Familienarchiv</div><div class="app-link on">Dokumente</div><div class="app-link">Personen</div><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">Keine Ergebnisse</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Briefe über gärten im frühling</div>
<div class="sfb-pill-wrap"><div class="sfb-pill-smart"><svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg> KI</div></div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<!-- Chips still visible -->
<div class="chip-row">
<div class="chip"><div class="chip-body"><span class="chip-pfx">Absender:</span><span class="chip-nm">Walter Raddatz</span></div><div class="chip-x">×</div></div>
<div class="chip"><div class="chip-body"><span class="chip-pfx">Stichwort:</span><span class="chip-nm">gärten</span></div><div class="chip-x">×</div></div>
</div>
<!-- Empty panel -->
<div class="status-area">
<svg class="status-empty-icon" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="#012851" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11" stroke-dasharray="2 1"/></svg>
<div class="status-empty-title">Keine Ergebnisse</div>
<div class="status-empty-body">Für „Briefe über gärten" wurden keine Dokumente gefunden. Versuchen Sie es mit einer einfachen Stichwortsuche.</div>
<a class="status-empty-link" role="button" tabindex="0">Als Volltextsuche wiederholen</a>
</div>
</div>
</div>
</div>
<p class="cap">Chips remain visible — the user sees what was searched. The fallback link switches to keyword mode in-place (Option A from issue): <code>smartMode = false</code>, keeps <code>q</code>, triggers keyword search. No navigation, no scroll reset. Min-h 44 px touch target on link.</p>
</div>
<!-- UNAVAILABLE 503 -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#b91c1c"></span>503 — SMART_SEARCH_UNAVAILABLE</div>
<div class="scale-outer" style="width:832px;height:390px">
<div class="scale-inner" style="height:600px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav"><div class="app-logo">Familienarchiv</div><div class="app-link on">Dokumente</div><div class="app-link">Personen</div><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">Intelligente Suche nicht verfügbar</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Was hat walter geschrieben?</div>
<div class="sfb-pill-wrap"><div class="sfb-pill-smart"><svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg> KI</div></div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<!-- Error panel -->
<div class="status-area">
<div class="status-icon-circle err">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#b91c1c" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><circle cx="12" cy="16" r="1" fill="#b91c1c"/></svg>
</div>
<div class="status-error-title">Intelligente Suche nicht verfügbar</div>
<div class="status-error-body">Die KI-Suche ist momentan nicht erreichbar. Sie können Ihre Anfrage als einfache Volltextsuche wiederholen.</div>
<button class="status-err-btn">Zur Volltextsuche wechseln</button>
</div>
<div style="text-align:center"><span class="ann">ErrorCode: SMART_SEARCH_UNAVAILABLE</span><span class="ann">i18n: search_error_unavailable + search_switch_to_keyword</span></div>
</div>
</div>
</div>
<p class="cap">503. Full-area error panel with icon, explanatory body text, and the keyword fallback button. The button calls <code>switchToKeywordMode()</code>. Separate <code>case</code> in <code>getErrorMessage()</code>.</p>
</div>
<!-- RATE LIMITED 429 -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#d97706"></span>429 — SMART_SEARCH_RATE_LIMITED</div>
<div class="scale-outer" style="width:832px;height:390px">
<div class="scale-inner" style="height:600px">
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div class="app-nav"><div class="app-logo">Familienarchiv</div><div class="app-link on">Dokumente</div><div class="app-link">Personen</div><div class="app-nav-r"><div class="app-av">M</div></div></div>
<div class="pg">
<div class="pg-h1">Dokumente</div>
<div class="pg-sub">Zu viele Anfragen</div>
<div class="sfb">
<div class="sfb-r1" style="margin-bottom:0">
<div class="sfb-input-wrap">
<div class="sfb-input typed">Was hat walter geschrieben?</div>
<div class="sfb-pill-wrap"><div class="sfb-pill-smart"><svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg> KI</div></div>
</div>
<div class="sfb-sort-btn">Sortierung ▾</div>
<div class="sfb-filter-btn"><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 8 8 12 12 8"/></svg> Filter</div>
<div class="sfb-reset-btn"></div>
</div>
</div>
<!-- Warning panel — no button -->
<div class="status-area">
<div class="status-icon-circle warn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><circle cx="12" cy="17" r="1" fill="#d97706"/></svg>
</div>
<div class="status-error-title">Zu viele Anfragen</div>
<div class="status-error-body">Du hast die intelligente Suche zu häufig genutzt.<br>Bitte warte eine Minute und versuche es erneut.</div>
<!-- Intentionally NO button — rate limit is temporary -->
</div>
<div style="text-align:center"><span class="ann">ErrorCode: SMART_SEARCH_RATE_LIMITED</span><span class="ann">No fallback button — limit is temporary</span><span class="ann">Separate case in getErrorMessage()</span></div>
</div>
</div>
</div>
<p class="cap">429. Warning-style panel with no keyword fallback button — the rate limit is temporary; the user should wait and retry. Do not group this <code>case</code> with UNAVAILABLE even if messages look similar.</p>
</div>
</div>
</div>
<!-- ══ SECTION 8 — MOBILE 320 px ═════════════════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>8 · Mobile (320 px) — toggle in input, chips wrapping</h2>
<p>The toggle pill stays inside the input on mobile too — the input is full-width anyway so there is room. Below the <code>sm</code> breakpoint (640 px) the pill label expands slightly to "KI-Suche" / "Textsuche" for senior legibility at small sizes. Chips use <code>flex flex-wrap</code> — no horizontal scroll at 320 px.</p>
</div>
<div class="split2">
<!-- Mobile keyword mode -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#A1DCD8;border:1px solid #ccc"></span>Mobile 320 px — keyword mode</div>
<div class="scale-outer-sm" style="width:263px;height:262px">
<div class="scale-inner-sm" style="height:320px">
<div style="height:18px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 8px;gap:4px"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div style="height:28px;background:#012851;display:flex;align-items:center;padding:0 10px"><div class="app-logo">Familienarchiv</div><div style="margin-left:auto"><div class="app-av">M</div></div></div>
<div class="pg" style="padding:10px">
<div class="pg-h1" style="font-size:14px;margin-bottom:2px">Dokumente</div>
<div class="pg-sub">Suche in Briefen und Urkunden</div>
<div class="sfb" style="padding:10px">
<!-- Input + pill (full width) -->
<div class="sfb-input-wrap" style="margin-bottom:8px">
<div class="sfb-input" style="padding-right:80px;height:44px;font-size:9px">Suche…</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-kw" style="font-size:7px;padding:3px 7px">
<svg width="8" height="8" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="1" y="3" width="14" height="10" rx="2"/><line x1="4" y1="7" x2="12" y2="7"/><line x1="4" y1="10" x2="9" y2="10"/></svg>
Textsuche
</div>
</div>
</div>
<!-- Sort + Filter row -->
<div style="display:flex;gap:5px">
<div class="sfb-sort-btn" style="height:36px;font-size:7.5px;padding:0 6px">Sortierung ▾</div>
<div class="sfb-filter-btn" style="height:36px;font-size:7.5px;padding:0 6px;flex:1;justify-content:center">Filter</div>
<div class="sfb-reset-btn" style="height:36px;width:32px"></div>
</div>
</div>
</div>
</div>
</div>
<p class="cap">Keyword mode on mobile. Pill reads "Textsuche" at narrow widths for senior legibility — abbreviated "Text" could be read as a noun. The input is 44 px tall (meets touch target). Sort and Filter buttons remain in the same row below.</p>
</div>
<!-- Mobile smart mode + chips -->
<div>
<div class="screen-lbl"><span class="lbl-dot" style="background:#012851"></span>Mobile 320 px — smart mode + chips wrapping</div>
<div class="scale-outer-sm" style="width:263px;height:360px">
<div class="scale-inner-sm" style="height:439px">
<div style="height:18px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 8px;gap:4px"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
<div style="height:28px;background:#012851;display:flex;align-items:center;padding:0 10px"><div class="app-logo">Familienarchiv</div><div style="margin-left:auto"><div class="app-av">M</div></div></div>
<div class="pg" style="padding:10px">
<div class="pg-h1" style="font-size:14px;margin-bottom:2px">Dokumente</div>
<div class="pg-sub">12 Ergebnisse</div>
<div class="sfb" style="padding:10px">
<div class="sfb-input-wrap" style="margin-bottom:8px">
<div class="sfb-input typed" style="padding-right:80px;height:44px;font-size:9px">Was hat walter im krieg…</div>
<div class="sfb-pill-wrap">
<div class="sfb-pill-smart" style="font-size:7px;padding:3px 7px">
<svg width="8" height="8" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l1.5 4.5H14l-3.7 2.7 1.4 4.3L8 10.2l-3.7 2.3 1.4-4.3L2 5.5h4.5z"/></svg>
KI-Suche
</div>
</div>
</div>
<div style="display:flex;gap:5px">
<div class="sfb-sort-btn" style="height:36px;font-size:7.5px;padding:0 6px">Sortierung ▾</div>
<div class="sfb-filter-btn" style="height:36px;font-size:7.5px;padding:0 6px;flex:1;justify-content:center">Filter</div>
<div class="sfb-reset-btn" style="height:36px;width:32px"></div>
</div>
</div>
<!-- Chips wrapping -->
<div class="chip-row" style="max-width:303px;gap:5px">
<div class="chip" style="font-size:8px">
<div class="chip-body" style="padding:4px 5px 4px 8px">
<span class="chip-pfx">Absender:</span>
<span style="max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:'Tinos',Georgia,serif;font-size:9px;color:#012851">Walter Raddatz</span>
</div>
<div class="chip-x" style="width:22px;font-size:9px;flex-shrink:0">×</div>
</div>
<div class="chip" style="font-size:8px">
<div class="chip-body" style="padding:4px 5px 4px 8px">
<span class="chip-pfx">Zeitraum:</span><span class="chip-nm" style="font-size:9px">19141918</span>
</div>
<div class="chip-x" style="width:22px;font-size:9px;flex-shrink:0">×</div>
</div>
<!-- Stichwort wraps to next line -->
<div class="chip" style="font-size:8px">
<div class="chip-body" style="padding:4px 5px 4px 8px">
<span class="chip-pfx">Stichwort:</span><span class="chip-nm" style="font-size:9px">krieg</span>
</div>
<div class="chip-x" style="width:22px;font-size:9px;flex-shrink:0">×</div>
</div>
</div>
<!-- Doc list -->
<div class="doc-list" style="gap:4px">
<div class="doc-row" style="padding:7px 8px"><div class="doc-thumb" style="width:18px;height:24px;font-size:5px"></div><div class="doc-info"><div class="doc-title" style="font-size:8.5px">Brief an Emma, August 1916</div><div class="doc-meta">Walter Raddatz · 1916</div></div></div>
<div class="doc-row" style="padding:7px 8px"><div class="doc-thumb" style="width:18px;height:24px;font-size:5px"></div><div class="doc-info"><div class="doc-title" style="font-size:8.5px">Feldpostkarte, Juni 1917</div><div class="doc-meta">Walter Raddatz · 1917</div></div></div>
</div>
</div>
</div>
</div>
<p class="cap">Smart mode on mobile. Pill reads "KI-Suche". The input is 44 px tall. Chips wrap at 303 px — "Stichwort: krieg" lands on the second line. No horizontal scroll. × buttons at 22 px wide, outside truncatable spans.</p>
</div>
</div>
</div>
<!-- ══ SECTION 9 — IMPLEMENTATION REFERENCE ══════════════════════════════════ -->
<div class="section">
<div class="sh">
<h2>9 · Implementation reference</h2>
</div>
<div class="rules">
<table>
<thead><tr><th>Element</th><th>Tailwind / CSS</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Input wrapper</td>
<td><code>relative flex-1</code></td>
<td>Position context for the absolutely-placed toggle pill</td>
</tr>
<tr>
<td>Search input — smart mode</td>
<td><code>pr-28</code> (≥ sm), <code>pr-24</code> (mobile) — extra right padding for pill</td>
<td>Also: <code>maxlength="500"</code> when <code>smartMode</code>; <code>oninput={smartMode ? undefined : onSearch}</code></td>
</tr>
<tr>
<td>Toggle pill wrapper</td>
<td><code>absolute right-2 top-1/2 -translate-y-1/2</code></td>
<td>Sits over the input's right padding; <code>pointer-events-auto</code> (parent input is pointer-events-none on the right side for the icon slot)</td>
</tr>
<tr>
<td>Pill — keyword (resting)</td>
<td><code>flex items-center gap-1.5 rounded-full border border-line bg-muted text-ink-2 text-[7.5px] font-bold px-2.5 py-1 min-h-[28px] cursor-pointer focus-visible:ring-2 focus-visible:ring-brand-navy outline-none</code></td>
<td><code>aria-pressed="false"</code>; icon + "Text" (desktop) / "Textsuche" (mobile, <code>sm:hidden</code>)</td>
</tr>
<tr>
<td>Pill — smart (active)</td>
<td><code>flex items-center gap-1.5 rounded-full border border-primary bg-primary text-primary-fg text-[7.5px] font-bold px-2.5 py-1 min-h-[28px] cursor-pointer focus-visible:ring-2 focus-visible:ring-brand-navy outline-none</code></td>
<td><code>aria-pressed="true"</code>; icon + "KI" (desktop) / "KI-Suche" (mobile). Matches AND/OR button active pattern</td>
</tr>
<tr>
<td>Chip wrapper</td>
<td><code>inline-flex items-stretch border-[1.5px] border-primary rounded-full bg-surface overflow-hidden focus-visible:ring-2 focus-visible:ring-brand-navy outline-none</code></td>
<td>Entire wrapper focusable — ring on wrapper, not only the × button</td>
</tr>
<tr>
<td>Chip body</td>
<td><code>flex items-center gap-1 pl-3 pr-2 py-1</code></td>
<td>Prefix: <code>text-[8px] font-bold opacity-65</code>; name: <code>font-serif text-[9.5px]</code></td>
</tr>
<tr>
<td>Chip name span (directional)</td>
<td><code>sm:max-w-[12rem] max-w-[8rem] truncate</code></td>
<td>Two separate spans; <code>&lt;span aria-hidden="true"&gt;&lt;/span&gt;</code> between. Chip: <code>aria-label="Von {p0} zu {p1}, Filter entfernen"</code></td>
</tr>
<tr>
<td>Chip × button</td>
<td><code>w-7 shrink-0 flex items-center justify-center border-l border-primary/15 text-[11px] text-ink min-h-[36px]</code></td>
<td><code>aria-label="Filter entfernen: {label}"</code>; min-h-[36px] extends touch target beyond visual size</td>
</tr>
<tr>
<td>Chip row</td>
<td><code>flex flex-wrap gap-2</code></td>
<td>Wraps at 320 px without horizontal scroll; no max-width on the row itself</td>
</tr>
<tr>
<td>SmartSearchStatus — loading</td>
<td><code>flex flex-col items-center justify-center gap-3 py-16 text-center</code></td>
<td>Spinner: <code>w-9 h-9 rounded-full border-[3px] border-primary/12 border-t-primary motion-safe:animate-spin</code>; title: <code>text-sm font-bold</code>; sub: <code>text-[9px] text-ink-3 max-w-[20rem]</code>. Wrapper: <code>role="status" aria-live="polite"</code></td>
</tr>
<tr>
<td>SmartSearchStatus — error (503)</td>
<td><code>flex flex-col items-center gap-3 py-16 text-center</code></td>
<td>Icon circle: <code>w-10 h-10 rounded-full bg-red-50 border-2 border-red-400 flex items-center justify-center</code>; action: <code>border border-primary text-primary px-4 py-2 text-[9px] font-bold rounded-sm</code></td>
</tr>
<tr>
<td>SmartSearchStatus — warning (429)</td>
<td>Same layout as 503 but amber icon; <strong>no action button</strong></td>
<td>Separate <code>case SMART_SEARCH_RATE_LIMITED</code> in <code>getErrorMessage()</code>. Do not group with 503</td>
</tr>
<tr>
<td>Empty state fallback link</td>
<td><code>text-primary font-bold text-[9px] underline underline-offset-2 focus-visible:ring-2 focus-visible:ring-brand-navy outline-none py-3 inline-block</code></td>
<td>Option A: <code>smartMode = false</code>, keep <code>q</code>, call <code>onSearch()</code>. No navigation. i18n: <code>search_empty_retry_keyword</code></td>
</tr>
<tr>
<td>Disambiguation chip</td>
<td>Same chip structure but <code>border-amber-600 bg-amber-50</code>; append "(auswählen…)" hint span in italics</td>
<td>Trigger: <code>aria-expanded aria-controls</code>; <code>min-h-[44px]</code>; <code>aria-label={m.search_disambiguation_trigger_label()}</code></td>
</tr>
<tr>
<td>Disambiguation picker panel</td>
<td><code>bg-surface border border-primary rounded-sm p-2</code></td>
<td>Focus moves to first item on open. Escape → return focus to trigger. Suchen → <code>GET /api/documents/search</code></td>
</tr>
</tbody>
</table>
</div>
<div class="sh" style="margin-top:28px">
<h2>9.1 · i18n keys (messages/{de,en,es}.json)</h2>
</div>
<div class="rules">
<table>
<thead><tr><th>Key</th><th>German</th><th>Used by</th></tr></thead>
<tbody>
<tr><td><code>search_toggle_smart_label</code></td><td>"KI" / "KI-Suche"</td><td>SmartModeToggle — smart mode</td></tr>
<tr><td><code>search_toggle_keyword_label</code></td><td>"Text" / "Textsuche"</td><td>SmartModeToggle — keyword mode</td></tr>
<tr><td><code>search_loading_nl</code></td><td>"Archiv wird befragt…"</td><td>SmartSearchStatus loading title</td></tr>
<tr><td><code>search_loading_nl_sub</code></td><td>"Die KI analysiert Ihre Anfrage. Das kann bis zu 15 Sekunden dauern."</td><td>SmartSearchStatus loading subtitle</td></tr>
<tr><td><code>search_error_unavailable</code></td><td>"Intelligente Suche nicht verfügbar"</td><td>SmartSearchStatus 503 title</td></tr>
<tr><td><code>search_error_unavailable_body</code></td><td>"Die KI-Suche ist momentan nicht erreichbar. Sie können Ihre Anfrage als einfache Volltextsuche wiederholen."</td><td>SmartSearchStatus 503 body</td></tr>
<tr><td><code>search_switch_to_keyword</code></td><td>"Zur Volltextsuche wechseln"</td><td>SmartSearchStatus 503 button</td></tr>
<tr><td><code>search_error_rate_limited</code></td><td>"Zu viele Anfragen"</td><td>SmartSearchStatus 429 title</td></tr>
<tr><td><code>search_error_rate_limited_body</code></td><td>"Du hast die intelligente Suche zu häufig genutzt. Bitte warte eine Minute und versuche es erneut."</td><td>SmartSearchStatus 429 body</td></tr>
<tr><td><code>search_empty_retry_keyword</code></td><td>"Als Volltextsuche wiederholen"</td><td>Empty state link</td></tr>
<tr><td><code>search_filter_remove_label</code></td><td>"Filter entfernen: {label}"</td><td>Chip × button aria-label</td></tr>
<tr><td><code>search_disambiguation_trigger_label</code></td><td>"Mehrere Personen gefunden — zum Auswählen klicken"</td><td>Disambiguation chip trigger</td></tr>
</tbody>
</table>
</div>
<div class="sh" style="margin-top:28px">
<h2>9.2 · Atomic ErrorCode rollout (one commit)</h2>
</div>
<div class="rules">
<table>
<thead><tr><th>Step</th><th>File</th><th>Change</th></tr></thead>
<tbody>
<tr><td>1</td><td><code>ErrorCode.java</code></td><td>Add <code>SMART_SEARCH_UNAVAILABLE</code>, <code>SMART_SEARCH_RATE_LIMITED</code></td></tr>
<tr><td>2</td><td><code>frontend/src/lib/shared/errors.ts</code></td><td>Add both to <code>ErrorCode</code> union type</td></tr>
<tr><td>3</td><td><code>errors.ts → getErrorMessage()</code></td><td>One separate <code>case</code> per code — do not group</td></tr>
<tr><td>4</td><td><code>messages/{de,en,es}.json</code></td><td>All 12 keys above in all three locale files</td></tr>
</tbody>
</table>
</div>
</div>
</div><!-- /doc -->
</body>
</html>