Files
mealprep/specs/frontend/e2-members-kachel.html
Marcel Raddatz 6dd0b7ac93 docs(specs): add final frontend specs for members and settings Kachel views
Finalised implementation specs for /members (E2) and /settings (E1)
pages using the chosen Kachel (card grid) variation. Members spec
covers 6 states including role-change inline control and remove
confirmation dialog; notes backend gaps (DELETE/PATCH member
endpoints). Settings spec covers hub layout, D3 staples sub-page,
hover and empty states.

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

906 lines
54 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>E2 — Mitglieder · Kachel-Ansicht · Finale Spezifikation</title>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<!--
spec:agent
document: E2 Mitglieder Kachel-Ansicht, Finale Spezifikation
version: 1.0
journey: J7 Manage household members
route: /members
screen: E2
chosen-variation: V2 Kachel-Ansicht (Card grid)
last-updated: 2026-04-09
BACKEND GAPS (must be implemented before this page can ship):
- DELETE /v1/households/mine/members/{userId} → remove member
- PATCH /v1/households/mine/members/{userId} → body: { role: "planer"|"mitglied" }
- GET /v1/households/mine/invites → list active invites with expiry
These endpoints do not exist in the current API schema (schema.d.ts).
Existing: GET /v1/households/mine/members, POST /v1/households/mine/invites
ROLE ACCESS:
- rolle === 'planer': sees kebab menu on all cards except own
- rolle === 'mitglied': sees all cards read-only, no kebab, no invite card CTA
-->
<style>
:root {
--color-page: #FAFAF7;
--color-surface: #F5F4EE;
--color-subtle: #EDECEA;
--color-border: #D8D7D0;
--color-text-muted: #6B6A63;
--color-text: #1C1C18;
--green-tint: #E8F5EA;
--green-light: #AEDCB0;
--green: #3D8C4A;
--green-dark: #2E6E39;
--yellow-tint: #FDF6D8;
--yellow-light: #F9E08A;
--yellow-text: #8A6800;
--color-error: #DC4C3E;
--error-tint: #FDECEA;
--blue-tint: #E6F1FB;
--blue: #185FA5;
--blue-dark: #0C447C;
--font-display: 'Fraunces', Georgia, serif;
--font-sans: 'DM Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', monospace;
--radius-sm: 4px; --radius-md: 6px; --radius-lg: 10px; --radius-xl: 16px; --radius-full: 9999px;
--shadow-card: 0 1px 3px rgba(28,28,24,.06), 0 1px 2px rgba(28,28,24,.04);
--shadow-raised: 0 4px 12px rgba(28,28,24,.10), 0 2px 4px rgba(28,28,24,.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: var(--font-sans); background: var(--color-page); color: var(--color-text); font-size: 14px; line-height: 1.6; }
/* ── Doc layout ── */
.doc { max-width: 1040px; margin: 0 auto; padding: 48px 40px 96px; }
.doc-header { padding-bottom: 28px; border-bottom: 1px solid var(--color-border); margin-bottom: 48px; display: flex; justify-content: space-between; align-items: flex-end; }
.doc-header h1 { font-family: var(--font-display); font-size: 26px; font-weight: 500; letter-spacing: -0.02em; }
.doc-header p { font-size: 13px; color: var(--color-text-muted); margin-top: 4px; }
.doc-meta { font-family: var(--font-mono); font-size: 11px; color: var(--color-text-muted); text-align: right; line-height: 1.9; }
.section-label { font-size: 10px; font-weight: 500; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-text-muted); padding-bottom: 10px; border-bottom: 1px solid var(--color-border); margin-bottom: 32px; margin-top: 56px; }
.intro { font-size: 14px; line-height: 1.75; max-width: 640px; margin-bottom: 40px; }
/* ── State sections ── */
.state { margin-bottom: 64px; }
.state-header { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
.state-id { font-family: var(--font-mono); font-size: 10px; font-weight: 500; background: var(--color-subtle); color: var(--color-text-muted); padding: 2px 8px; border-radius: var(--radius-sm); white-space: nowrap; margin-top: 3px; }
.state-title { font-size: 16px; font-weight: 500; letter-spacing: -0.01em; }
.state-desc { font-size: 13px; color: var(--color-text-muted); margin-top: 2px; max-width: 540px; }
/* ── Preview ── */
.preview-wrap { display: flex; gap: 24px; align-items: flex-start; margin-bottom: 20px; }
.preview-d-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-m-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.preview-label { font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: var(--color-text-muted); }
.preview-d-clip { height: 340px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-page); }
.preview-d-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
.preview-m-clip { width: 196px; height: 340px; overflow: hidden; border: 1.5px solid var(--color-border); border-radius: 24px; background: var(--color-page); }
.preview-m-scale { transform: scale(0.5); transform-origin: top left; width: 200%; }
/* ── Notes ── */
.notes { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: 14px 18px; }
.notes-label { font-size: 9px; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-text-muted); margin-bottom: 8px; }
.notes ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.notes li { font-size: 12px; color: var(--color-text-muted); line-height: 1.5; display: flex; align-items: flex-start; gap: 8px; }
.notes li::before { content: '→'; color: var(--green); font-weight: 500; flex-shrink: 0; }
.notes li.warn::before { content: '⚠'; color: var(--yellow-text); }
.notes li.gap::before { content: '✗'; color: var(--color-error); }
/* ── Warning banner ── */
.backend-warning { background: var(--yellow-tint); border: 1px solid var(--yellow-light); border-radius: var(--radius-lg); padding: 14px 18px; margin-bottom: 40px; }
.backend-warning h3 { font-size: 12px; font-weight: 600; color: var(--yellow-text); margin-bottom: 6px; }
.backend-warning ul { list-style: none; display: flex; flex-direction: column; gap: 3px; }
.backend-warning li { font-family: var(--font-mono); font-size: 11px; color: var(--yellow-text); display: flex; gap: 8px; }
.backend-warning li::before { content: '○'; }
/* ── AppShell chrome ── */
.shell { display: flex; min-height: 100vh; background: var(--color-page); font-family: var(--font-sans); }
.sidebar { width: 224px; min-width: 224px; background: white; border-right: 1px solid var(--color-border); display: flex; flex-direction: column; }
.sidebar-brand { padding: 14px 18px; border-bottom: 1px solid var(--color-border); }
.sidebar-brand-row { display: flex; align-items: center; gap: 8px; }
.sidebar-logo { width: 22px; height: 22px; background: var(--green); border-radius: var(--radius-sm); }
.sidebar-app { font-family: var(--font-display); font-size: 15px; font-weight: 500; }
.sidebar-household { font-size: 10px; color: var(--color-text-muted); margin-top: 1px; }
.sidebar-nav { flex: 1; padding: 4px 8px; }
.sidebar-group-label { font-size: 8px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-text-muted); padding: 16px 12px 4px; }
.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: var(--radius-md); font-size: 13px; color: var(--color-text); text-decoration: none; }
.sidebar-item.active { background: var(--green-tint); color: var(--green-dark); font-weight: 500; }
.sidebar-item:not(.active):hover { background: var(--color-subtle); }
.sidebar-icon { width: 20px; text-align: center; font-size: 16px; }
/* ── Page content ── */
.page-content { flex: 1; padding: 32px 40px; }
.page-title { font-family: var(--font-display); font-size: 24px; font-weight: 500; letter-spacing: -0.02em; margin-bottom: 4px; }
.page-subtitle { font-size: 13px; color: var(--color-text-muted); margin-bottom: 28px; }
/* ── Member card grid ── */
.member-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.member-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; box-shadow: var(--shadow-card); position: relative; display: flex; flex-direction: column; align-items: center; text-align: center; }
.member-card.hovered { box-shadow: var(--shadow-raised); border-color: #C0BFB8; }
.member-card.own { border-color: var(--green-light); }
.avatar { width: 56px; height: 56px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 20px; font-weight: 500; color: white; margin-bottom: 12px; flex-shrink: 0; }
.avatar-planer { background: var(--green-dark); }
.avatar-mitglied { background: var(--blue); }
.member-name { font-size: 14px; font-weight: 500; margin-bottom: 6px; }
.role-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); white-space: nowrap; }
.role-badge.planer { background: var(--green-tint); color: var(--green-dark); }
.role-badge.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
.join-date { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
.self-badge { font-size: 10px; font-weight: 500; letter-spacing: 0.04em; padding: 2px 8px; border-radius: var(--radius-full); background: var(--green-tint); color: var(--green-dark); }
/* ── Kebab button ── */
.kebab-btn { position: absolute; top: 12px; right: 12px; width: 28px; height: 28px; border-radius: var(--radius-md); background: transparent; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--color-text-muted); }
.kebab-btn:hover, .kebab-btn.open { background: var(--color-subtle); color: var(--color-text); }
/* ── Dropdown menu ── */
.dropdown { position: absolute; top: 44px; right: 12px; background: white; border: 1px solid var(--color-border); border-radius: var(--radius-lg); box-shadow: var(--shadow-raised); min-width: 160px; z-index: 10; overflow: hidden; }
.dropdown-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; color: var(--color-text); cursor: pointer; white-space: nowrap; }
.dropdown-item:hover { background: var(--color-subtle); }
.dropdown-item.danger { color: var(--color-error); }
.dropdown-item.danger:hover { background: var(--error-tint); }
.dropdown-icon { font-size: 14px; width: 16px; text-align: center; }
.dropdown-divider { height: 1px; background: var(--color-border); margin: 2px 0; }
/* ── Role segmented control (inline on card) ── */
.role-control { display: flex; border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; margin-top: 8px; width: 100%; }
.role-control-btn { flex: 1; padding: 6px 8px; font-size: 11px; font-weight: 500; background: white; border: none; cursor: pointer; color: var(--color-text-muted); }
.role-control-btn.active { background: var(--green-dark); color: white; }
.role-control-btn:first-child { border-right: 1px solid var(--color-border); }
/* ── Invite card ── */
.invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 24px 20px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; cursor: pointer; min-height: 180px; gap: 10px; }
.invite-card:hover { border-color: var(--green-light); background: var(--green-tint); }
.invite-plus { width: 44px; height: 44px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--color-text-muted); }
.invite-card:hover .invite-plus { background: var(--green-light); color: var(--green-dark); }
.invite-label { font-size: 13px; font-weight: 500; color: var(--color-text-muted); }
.invite-card:hover .invite-label { color: var(--green-dark); }
/* ── Invite panel (expanded inline) ── */
.invite-panel { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 24px; margin-top: 8px; }
.invite-panel-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.invite-panel-desc { font-size: 12px; color: var(--color-text-muted); margin-bottom: 16px; }
.invite-link-row { display: flex; gap: 8px; align-items: center; }
.invite-link-box { flex: 1; background: var(--color-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 8px 12px; font-family: var(--font-mono); font-size: 12px; color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-copy { padding: 8px 14px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; }
.btn-copy:hover { background: var(--color-subtle); }
.invite-expiry { font-size: 11px; color: var(--color-text-muted); margin-top: 8px; }
.invite-expiry span { background: var(--yellow-tint); color: var(--yellow-text); padding: 1px 6px; border-radius: var(--radius-sm); font-weight: 500; }
.btn-regen { margin-top: 12px; font-size: 12px; color: var(--color-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; }
.btn-regen:hover { color: var(--color-text); }
/* ── Dialog overlay ── */
.overlay { position: absolute; inset: 0; background: rgba(28,28,24,.45); display: flex; align-items: center; justify-content: center; z-index: 50; }
.dialog { background: white; border-radius: var(--radius-xl); padding: 28px 32px; max-width: 380px; width: 100%; box-shadow: var(--shadow-raised); }
.dialog-title { font-size: 16px; font-weight: 500; margin-bottom: 8px; }
.dialog-body { font-size: 13px; color: var(--color-text-muted); line-height: 1.6; margin-bottom: 24px; }
.dialog-body strong { color: var(--color-text); font-weight: 500; }
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
.btn-cancel { padding: 9px 18px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 13px; font-weight: 500; cursor: pointer; }
.btn-cancel:hover { background: var(--color-subtle); }
.btn-remove { padding: 9px 18px; border-radius: var(--radius-md); border: none; background: var(--color-error); color: white; font-size: 13px; font-weight: 500; cursor: pointer; }
.btn-remove:hover { background: #C43A2E; }
/* ── Mobile shell ── */
.m-shell { display: flex; flex-direction: column; background: var(--color-page); }
.m-header { padding: 16px; background: white; border-bottom: 1px solid var(--color-border); display: flex; align-items: center; justify-content: space-between; }
.m-header-title { font-size: 16px; font-weight: 500; }
.m-header-btn { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--green-dark); display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; border: none; cursor: pointer; }
.m-content { flex: 1; padding: 16px; }
.m-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.m-card { background: white; border: 1px solid var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; text-align: center; position: relative; box-shadow: var(--shadow-card); }
.m-avatar { width: 44px; height: 44px; border-radius: var(--radius-full); display: flex; align-items: center; justify-content: center; font-family: var(--font-display); font-size: 16px; font-weight: 500; color: white; margin-bottom: 8px; }
.m-avatar.planer { background: var(--green-dark); }
.m-avatar.mitglied { background: var(--blue); }
.m-name { font-size: 12px; font-weight: 500; margin-bottom: 4px; }
.m-role { font-size: 10px; font-weight: 500; padding: 2px 6px; border-radius: var(--radius-full); }
.m-role.planer { background: var(--green-tint); color: var(--green-dark); }
.m-role.mitglied { background: var(--blue-tint); color: var(--blue-dark); }
.m-kebab { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; color: var(--color-text-muted); background: none; border: none; }
.m-invite-card { background: white; border: 1.5px dashed var(--color-border); border-radius: var(--radius-xl); padding: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 120px; gap: 6px; }
.m-invite-plus { width: 36px; height: 36px; border-radius: var(--radius-full); background: var(--color-subtle); display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--color-text-muted); }
.m-invite-label { font-size: 11px; color: var(--color-text-muted); font-weight: 500; }
.m-tabbar { display: flex; border-top: 1px solid var(--color-border); background: white; }
.m-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 8px 4px 4px; font-size: 10px; color: var(--color-text-muted); gap: 2px; }
.m-tab.active { color: var(--green-dark); }
.m-tab-icon { font-size: 20px; }
/* ── Agent section ── */
.agent-section { background: var(--color-text); color: #E8E8E2; padding: 40px 48px; margin-top: 64px; }
.agent-section h2 { font-size: 10px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #6B6A63; margin-bottom: 4px; }
.agent-section > p { font-size: 13px; color: #9A9990; margin-bottom: 28px; line-height: 1.6; max-width: 640px; }
.spec-comment { font-family: var(--font-mono); font-size: 11px; color: #3A3A36; margin-bottom: 32px; line-height: 1.9; white-space: pre-wrap; }
.agent-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 11px; margin-bottom: 40px; }
.agent-table thead tr { border-bottom: 1px solid #2A2A26; }
.agent-table th { text-align: left; padding: 8px 14px; font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #5A5A55; font-family: var(--font-sans); }
.agent-table td { padding: 9px 14px; border-bottom: 1px solid #1E1E1A; vertical-align: top; line-height: 1.5; }
.agent-table tr:last-child td { border-bottom: none; }
.agent-table td:first-child { color: #7A7A72; white-space: nowrap; }
.agent-table td:nth-child(2) { color: #E8E8E2; font-weight: 500; }
.agent-table td:nth-child(3) { color: #5A5A55; }
.group-row td { padding-top: 20px; font-family: var(--font-sans); font-size: 9px; font-weight: 500; letter-spacing: 0.09em; text-transform: uppercase; color: #3A3A36; border-bottom: none; }
</style>
</head>
<body>
<div class="doc">
<!-- Header -->
<div class="doc-header">
<div>
<h1>E2 — Mitglieder</h1>
<p>Kachel-Ansicht · Finale Spezifikation · Route: <code>/members</code></p>
</div>
<div class="doc-meta">
screen: E2<br/>
journey: J7<br/>
variation: Kachel (V2)<br/>
version: 1.0<br/>
date: 2026-04-09
</div>
</div>
<p class="intro">
Die Mitgliederseite zeigt alle Haushaltsmitglieder als Kacheln. Der Planer kann Rollen ändern und Mitglieder
entfernen über ein Kebab-Menü auf jeder Kachel. Eine Einladekachel ermöglicht das Generieren und Kopieren des
Einlade-Links. Mitglieder sehen alle Kacheln nur lesend.
</p>
<div class="backend-warning">
<h3>Backend-Lücken — vor Implementierung schließen</h3>
<ul>
<li>DELETE /v1/households/mine/members/{userId} — Mitglied entfernen</li>
<li>PATCH /v1/households/mine/members/{userId} — Rolle ändern (body: { role })</li>
<li>GET /v1/households/mine/invites — aktive Einladungen auflisten (inkl. expiresAt)</li>
</ul>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S1 — Standardansicht (Planer)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S1</div>
<div>
<div class="state-title">Standardansicht — Planer sieht vollständige Kacheln</div>
<div class="state-desc">Alle Mitglieder als Kacheln, dahinter die Einladekachel. Kebab-Button erscheint on hover.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-row">
<div class="sidebar-logo"></div>
<span class="sidebar-app">Mealplan</span>
</div>
<div class="sidebar-household">Familie Raddatz</div>
</div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<!-- Own card -->
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Member 2 -->
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
</div>
<!-- Member 3 -->
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<!-- Invite card -->
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;">
<div class="m-header">
<span class="m-header-title">Mitglieder</span>
<button class="m-header-btn">+</button>
</div>
<div class="m-content">
<div class="m-grid">
<div class="m-card" style="border-color:var(--green-light);">
<div class="m-avatar planer">MR</div>
<div class="m-name">Marcel R.</div>
<span class="m-role planer">Planer</span>
<div style="margin-top:6px;font-size:10px;color:var(--color-text-muted);">Du</div>
</div>
<div class="m-card">
<button class="m-kebab"></button>
<div class="m-avatar mitglied">SR</div>
<div class="m-name">Sandra R.</div>
<span class="m-role mitglied">Mitglied</span>
</div>
<div class="m-card">
<button class="m-kebab"></button>
<div class="m-avatar mitglied">LR</div>
<div class="m-name">Lena R.</div>
<span class="m-role mitglied">Mitglied</span>
</div>
<div class="m-invite-card">
<div class="m-invite-plus">+</div>
<div class="m-invite-label">Einladen</div>
</div>
</div>
</div>
<div class="m-tabbar">
<div class="m-tab"><div class="m-tab-icon">📅</div>Planer</div>
<div class="m-tab"><div class="m-tab-icon">🍽</div>Rezepte</div>
<div class="m-tab"><div class="m-tab-icon">🛒</div>Einkauf</div>
<div class="m-tab active"><div class="m-tab-icon">⚙️</div>Einstellungen</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Eigene Kachel (Du): grüner Kartenrahmen (<code>border: var(--green-light)</code>), "Du"-Badge statt Kebab</li>
<li>Kebab-Button (<code></code>): immer im DOM, <code>opacity:0</code> bis hover/focus, dann <code>opacity:1</code>. Auf Touch-Geräten immer sichtbar.</li>
<li>Avatar-Initialen: erste zwei Buchstaben des displayName. Planer = green-dark, Mitglied = blue</li>
<li>Kachel-Reihenfolge: eigene Kachel immer zuerst, dann joinedAt aufsteigend, Einladekachel immer zuletzt</li>
<li>Mobile: "+" Button in der Header-Zeile öffnet Einlade-Panel. Einladekachel bleibt zusätzlich im Grid.</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S2 — Kebab-Menü offen</div>
<div class="state">
<div class="state-header">
<div class="state-id">S2</div>
<div>
<div class="state-title">Kebab-Menü geöffnet</div>
<div class="state-desc">Klick auf ⋯ öffnet Dropdown mit zwei Aktionen. Klick außerhalb schließt das Menü.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Menü offen auf "Sandra R."</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Card with open menu -->
<div class="member-card hovered" style="z-index:20;">
<button class="kebab-btn open"></button>
<div class="dropdown">
<div class="dropdown-item"><span class="dropdown-icon">🔄</span>Rolle ändern</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item danger"><span class="dropdown-icon"></span>Entfernen</div>
</div>
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
</div>
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- No mobile preview needed for this state; same as desktop but full-screen -->
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Dropdown: <code>position: absolute; top: 44px; right: 12px</code> relativ zur Kachel</li>
<li>Zwei Einträge: "Rolle ändern" (neutrales Icon 🔄) und "Entfernen" (rot, Icon ✕)</li>
<li>Klick außerhalb des Dropdowns schließt diesen (click-away listener)</li>
<li>Nur ein Menü gleichzeitig offen. ESC schließt ebenfalls.</li>
<li>Mobile: Tap auf ⋯ öffnet Bottom Sheet mit denselben zwei Einträgen (44px min-height pro Eintrag)</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S3 — Rolle ändern (inline auf der Kachel)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S3</div>
<div>
<div class="state-title">Rolle ändern — Segmented Control erscheint</div>
<div class="state-desc">Wahl von "Rolle ändern" ersetzt das Rolle-Badge durch einen 2-Button-Schalter [Planer | Mitglied]. Aktive Rolle vorausgewählt. Bestätigung sofort mit PATCH-Request.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Rolle-Control auf "Sandra R." aktiv</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<!-- Card in role-edit mode -->
<div class="member-card" style="border-color:#B5D4F4;">
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<!-- Role control replaces badge -->
<div class="role-control" style="width:100%;">
<button class="role-control-btn">Planer</button>
<button class="role-control-btn active">Mitglied</button>
</div>
<div class="join-date">seit 03.04.2026</div>
<button style="margin-top:8px;font-size:11px;color:var(--color-text-muted);background:none;border:none;cursor:pointer;text-decoration:underline;">Abbrechen</button>
</div>
<div class="member-card">
<button class="kebab-btn"></button>
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<div class="invite-card">
<div class="invite-plus">+</div>
<div class="invite-label">Mitglied einladen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Role-Control ersetzt das Badge in-place auf der Kachel. Kein Dialog, kein Page-Change.</li>
<li>Klick auf die inaktive Rolle → optimistisches Update → PATCH /v1/households/mine/members/{userId} { role }</li>
<li>Bei Erfolg: Role-Control durch neues Badge ersetzen</li>
<li>Bei Fehler: Rollback + Toast "Rolle konnte nicht geändert werden."</li>
<li>"Abbrechen" bringt ohne PATCH-Call das Badge zurück</li>
<li>Der Planer kann seinen eigenen Planer-Status nicht abgeben, solange er der einzige Planer ist</li>
<li>Kachel bekommt blauen Rahmen (<code>border-color: #B5D4F4</code>) als Editier-Indikator</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S4 — Entfernen-Bestätigung (Dialog)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S4</div>
<div>
<div class="state-title">Bestätigungsdialog "Mitglied entfernen"</div>
<div class="state-desc">Klick auf "Entfernen" im Dropdown öffnet einen modalen Dialog. Kein direktes Löschen ohne Bestätigung.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Dialog über der Seite</div>
<div class="preview-d-clip">
<div class="preview-d-scale" style="position:relative;">
<div class="shell" style="position:relative;">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
</div>
</div>
<div class="page-content" style="opacity:0.4;pointer-events:none;">
<div class="page-title">Mitglieder</div>
<div class="member-grid">
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span></div>
<div class="member-card"><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span></div>
<div class="member-card"><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span></div>
<div class="invite-card"><div class="invite-plus">+</div><div class="invite-label">Mitglied einladen</div></div>
</div>
</div>
<!-- Dialog -->
<div class="overlay" style="position:absolute;">
<div class="dialog">
<div class="dialog-title">Mitglied entfernen?</div>
<div class="dialog-body"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt und verliert sofort den Zugang zu allen Plänen und Rezepten.</div>
<div class="dialog-actions">
<button class="btn-cancel">Abbrechen</button>
<button class="btn-remove">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="preview-m-wrap">
<div class="preview-label">Mobile</div>
<div class="preview-m-clip">
<div class="preview-m-scale">
<div class="m-shell" style="min-height:680px;position:relative;">
<div class="m-header"><span class="m-header-title">Mitglieder</span><button class="m-header-btn">+</button></div>
<div class="m-content" style="opacity:0.35;pointer-events:none;">
<div class="m-grid">
<div class="m-card"><div class="m-avatar planer">MR</div><div class="m-name">Marcel R.</div><span class="m-role planer">Planer</span></div>
<div class="m-card"><div class="m-avatar mitglied">SR</div><div class="m-name">Sandra R.</div><span class="m-role mitglied">Mitglied</span></div>
</div>
</div>
<!-- Mobile dialog -->
<div class="overlay" style="position:absolute;align-items:flex-end;padding-bottom:0;">
<div class="dialog" style="border-radius:var(--radius-xl) var(--radius-xl) 0 0;max-width:100%;padding:24px 24px 32px;">
<div class="dialog-title" style="font-size:15px;">Mitglied entfernen?</div>
<div class="dialog-body" style="font-size:12px;"><strong>Sandra R.</strong> wird aus dem Haushalt entfernt.</div>
<div class="dialog-actions">
<button class="btn-cancel" style="font-size:12px;">Abbrechen</button>
<button class="btn-remove" style="font-size:12px;">Entfernen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Dialog zeigt den <strong>displayName</strong> des Mitglieds explizit</li>
<li>Bestätigung → DELETE /v1/households/mine/members/{userId} → Kachel aus Grid entfernen</li>
<li>Planer kann sich nicht selbst entfernen (eigene Kachel hat kein Kebab-Menü)</li>
<li>Letzter verbleibender Planer kann nicht entfernt werden → Fehlermeldung im Dialog</li>
<li>Mobile: Dialog als Bottom Sheet (<code>border-radius</code> nur oben, kein max-width)</li>
<li>Hintergrund leicht gedimmt: <code>rgba(28,28,24,.45)</code>, Klick außerhalb schließt nicht (explizite Bestätigung erforderlich)</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S5 — Einladekachel: Einlade-Panel</div>
<div class="state">
<div class="state-header">
<div class="state-id">S5</div>
<div>
<div class="state-title">Einlade-Panel — nach Klick auf die Einladekachel</div>
<div class="state-desc">Kachel expandiert zum Panel unterhalb der Grid-Reihe. Zeigt generierten Link + Ablaufdatum + Regenerieren-Option.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid">
<div class="member-card own"><div class="avatar avatar-planer">MR</div><div class="member-name">Marcel R.</div><span class="role-badge planer">Planer</span><div class="join-date">seit 02.04.2026</div><div style="margin-top:8px;"><span class="self-badge">Du</span></div></div>
<div class="member-card"><button class="kebab-btn"></button><div class="avatar avatar-mitglied">SR</div><div class="member-name">Sandra R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 03.04.2026</div></div>
<div class="member-card"><button class="kebab-btn"></button><div class="avatar avatar-mitglied">LR</div><div class="member-name">Lena R.</div><span class="role-badge mitglied">Mitglied</span><div class="join-date">seit 05.04.2026</div></div>
<div class="invite-card" style="border-color:var(--green-light);background:var(--green-tint);">
<div class="invite-plus" style="background:var(--green-light);color:var(--green-dark);">+</div>
<div class="invite-label" style="color:var(--green-dark);">Mitglied einladen</div>
</div>
</div>
<!-- Invite panel below grid -->
<div class="invite-panel" style="margin-top:16px;">
<div class="invite-panel-title">Einladelink teilen</div>
<div class="invite-panel-desc">Wer diesen Link öffnet, kann dem Haushalt als Mitglied beitreten.</div>
<div class="invite-link-row">
<div class="invite-link-box">https://mealplan.app/join/X4K9-RZMQ</div>
<button class="btn-copy">Kopieren</button>
</div>
<div class="invite-expiry">Läuft ab: <span>12.04.2026</span></div>
<button class="btn-regen">Neuen Link generieren</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Klick auf Einladekachel → POST /v1/households/mine/invites (falls kein aktiver Code vorhanden) oder GET /v1/households/mine/invites</li>
<li>Invite-Panel erscheint unterhalb der Grid-Reihe (kein Modal, kein Page-Change)</li>
<li>"Kopieren" → navigator.clipboard.writeText(shareUrl) → Button zeigt kurz "Kopiert ✓"</li>
<li>"Neuen Link generieren" → POST /v1/households/mine/invites → alten Code invalidieren → neuen Code anzeigen</li>
<li>Ablaufdatum <code>expiresAt</code> in gelbem Badge wenn ≤ 24h verbleibend</li>
<li>Nur Planer sehen den Einlade-CTA. Mitglied sieht keine Einladekachel.</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<div class="section-label">S6 — Mitglied-Perspektive (read-only)</div>
<div class="state">
<div class="state-header">
<div class="state-id">S6</div>
<div>
<div class="state-title">Ansicht als Haushaltsmitglied (rolle = mitglied)</div>
<div class="state-desc">Mitglieder sehen die Kacheln ohne Kebab-Menü und ohne Einladekachel.</div>
</div>
</div>
<div class="preview-wrap">
<div class="preview-d-wrap">
<div class="preview-label">Desktop — Mitglied-Perspektive</div>
<div class="preview-d-clip">
<div class="preview-d-scale">
<div class="shell">
<div class="sidebar" style="width:224px;min-width:224px;">
<div class="sidebar-brand"><div class="sidebar-brand-row"><div class="sidebar-logo"></div><span class="sidebar-app">Mealplan</span></div><div class="sidebar-household">Familie Raddatz</div></div>
<div class="sidebar-nav">
<div class="sidebar-group-label">Plan</div>
<a class="sidebar-item" href="#"><span class="sidebar-icon">📅</span>Planer</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🍽</span>Rezepte</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">🛒</span>Einkauf</a>
<div class="sidebar-group-label">Haushalt</div>
<a class="sidebar-item active" href="#"><span class="sidebar-icon">👥</span>Mitglieder</a>
<a class="sidebar-item" href="#"><span class="sidebar-icon">⚙️</span>Einstellungen</a>
</div>
</div>
<div class="page-content">
<div class="page-title">Mitglieder</div>
<div class="page-subtitle">3 Mitglieder · Familie Raddatz</div>
<div class="member-grid" style="grid-template-columns:repeat(3,1fr);">
<div class="member-card own">
<div class="avatar avatar-mitglied">SR</div>
<div class="member-name">Sandra R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 03.04.2026</div>
<div style="margin-top:8px;"><span class="self-badge">Du</span></div>
</div>
<div class="member-card">
<div class="avatar avatar-planer">MR</div>
<div class="member-name">Marcel R.</div>
<span class="role-badge planer">Planer</span>
<div class="join-date">seit 02.04.2026</div>
</div>
<div class="member-card">
<div class="avatar avatar-mitglied">LR</div>
<div class="member-name">Lena R.</div>
<span class="role-badge mitglied">Mitglied</span>
<div class="join-date">seit 05.04.2026</div>
</div>
<!-- No invite card for members -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="notes-label">Notizen</div>
<ul>
<li>Mitglied sieht keine Einladekachel und keine Kebab-Buttons auf anderen Kacheln</li>
<li>Eigene Kachel zeigt "Du"-Badge (grüner Rahmen), aber kein Kebab</li>
<li>Grid passt sich an: bei 3 Kacheln → <code>grid-template-columns: repeat(3, 1fr)</code> (kein leerer Slot für Einladen)</li>
<li>Server-seitige Prüfung: Aktionen (DELETE, PATCH) geben 403 für nicht-Planer zurück</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════ -->
<!-- ─── Machine-readable agent section ─── -->
<div class="agent-section">
<h2>Maschinen-lesbare Spezifikation</h2>
<p>Diese Sektion enthält verbindliche Implementierungsregeln für den Coding-Agenten.</p>
<pre class="spec-comment">
/* spec:rules — E2 Mitglieder Kachel
*
* LAYOUT
* grid: repeat(4, 1fr) gap 16px desktop; repeat(2, 1fr) gap 12px mobile
* card: bg white, border 1px solid --color-border, border-radius --radius-xl
* card padding: 24px 20px 20px desktop; 16px mobile
*
* AVATAR
* size: 56px desktop / 44px mobile; border-radius 50%
* initials: first two chars of displayName, uppercase
* planer color: --green-dark (#2E6E39)
* mitglied color: --blue (#185FA5)
*
* ROLE BADGE
* planer: bg --green-tint, color --green-dark
* mitglied: bg --blue-tint, color --blue-dark
* font-size 10px, font-weight 500, padding 2px 8px, border-radius --radius-full
*
* OWN CARD (benutzer.id === member.userId)
* border-color: --green-light
* show "Du" badge below join-date
* hide kebab button entirely
*
* KEBAB BUTTON
* position absolute, top 12px, right 12px
* opacity 0 by default; 1 on card:hover, card:focus-within, touch devices always 1
* opens dropdown: [Rolle ändern, divider, Entfernen(danger)]
* click-away closes; ESC closes
*
* ROLE CHANGE (S3)
* replaces badge in-place with segmented control [Planer | Mitglied]
* active button: bg --green-dark, color white
* inactive button: bg white, color --color-text-muted
* on select: PATCH /v1/households/mine/members/{userId} body { role }
* optimistic update; on error: rollback + toast
* Abbrechen link below control: reverts to badge without API call
* guard: planer cannot demote self if sole planer
*
* REMOVE CONFIRM (S4)
* modal dialog, backdrop rgba(28,28,24,.45), backdrop does NOT close on click
* shows member displayName in body text
* confirm → DELETE /v1/households/mine/members/{userId}
* on success: remove card from grid with fade-out
* mobile: bottom-sheet (border-radius top only)
*
* INVITE (S5)
* invite card always last in grid, only visible to planer
* click → POST /v1/households/mine/invites OR GET /v1/households/mine/invites
* panel below grid (not modal)
* copy: navigator.clipboard.writeText(shareUrl) → button text "Kopiert ✓" for 2s
* regenerate: POST new invite → invalidate old
* expiresAt badge yellow if ≤ 24h remaining
*
* MEMBER VIEW (S6)
* rolle === 'mitglied': hide all kebab buttons, hide invite card
* grid auto-adjusts columns (no empty slot)
*
* CARD ORDER
* 1. own card (benutzer.id === userId)
* 2. other members sorted by joinedAt ASC
* 3. invite card (planer only)
*
* BACKEND GAPS (must exist before page ships)
* DELETE /v1/households/mine/members/{userId}
* PATCH /v1/households/mine/members/{userId} body: { role: "planer"|"mitglied" }
* GET /v1/households/mine/invites
*/
</pre>
<table class="agent-table">
<thead>
<tr><th>Property</th><th>Value</th><th>Notes</th></tr>
</thead>
<tbody>
<tr class="group-row"><td colspan="3">Component: MemberCard</td></tr>
<tr><td>card-width</td><td>1fr (grid)</td><td>4-col desktop, 2-col mobile</td></tr>
<tr><td>card-min-height</td><td>180px</td><td>desktop; auto mobile</td></tr>
<tr><td>avatar-size</td><td>56px / 44px</td><td>desktop / mobile</td></tr>
<tr><td>avatar-radius</td><td>50%</td><td>full circle</td></tr>
<tr><td>kebab-target</td><td>44×44px</td><td>WCAG 2.2 minimum touch target</td></tr>
<tr><td>dropdown-min-width</td><td>160px</td><td>right-aligned to kebab</td></tr>
<tr class="group-row"><td colspan="3">Role Control</td></tr>
<tr><td>control-height</td><td>32px</td><td>segmented, full card width</td></tr>
<tr><td>active-bg</td><td>--green-dark</td><td>selected role button</td></tr>
<tr><td>api-endpoint</td><td>PATCH /v1/households/mine/members/{userId}</td><td>body: { role }</td></tr>
<tr class="group-row"><td colspan="3">Remove Dialog</td></tr>
<tr><td>confirm-btn-bg</td><td>--color-error (#DC4C3E)</td><td>danger action</td></tr>
<tr><td>api-endpoint</td><td>DELETE /v1/households/mine/members/{userId}</td><td></td></tr>
<tr><td>backdrop</td><td>rgba(28,28,24,.45)</td><td>click-outside does NOT close</td></tr>
<tr class="group-row"><td colspan="3">Invite</td></tr>
<tr><td>api-create</td><td>POST /v1/households/mine/invites</td><td>returns InviteResponse</td></tr>
<tr><td>api-list</td><td>GET /v1/households/mine/invites</td><td>backend gap</td></tr>
<tr><td>copy-feedback</td><td>"Kopiert ✓" for 2000ms</td><td>then revert to "Kopieren"</td></tr>
</tbody>
</table>
</div>
</div>
</body>
</html>