errors: none
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Documents page with a preview</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root { color-scheme: dark; --side: #0d0e10; --bg: #131416; --raised: #18191c; --hover: #1d1e22; --sel: #212329; --border: #25272b; --border-2: #2f3136; --border-3: #3a3d44; --bg-2: #101216; --text: #e6e7ea; --text-2: #a9acb3; --text-3: #83868e; --text-4: #6b6e76; --accent: #5e64c9; --accent-text: #a3a8f0; --accent-dim: #2a2d5a; --blocking: #d9a441; --needs-text: #e8c27c; --needs-bg: color-mix(in srgb, var(--blocking) 13%, transparent); --needs-line: color-mix(in srgb, var(--blocking) 46%, var(--border-2)); --font: "Geist", -apple-system, "Segoe UI", system-ui, sans-serif; --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace; }
* { box-sizing: border-box; }
html, body { margin: 0; background: #0a0b0c; color: var(--text); font-family: var(--font); font-size: 13px; line-height: 1.45; -webkit-font-smoothing: antialiased; }
a { color: var(--accent-text); text-decoration: none; }
a:hover { color: var(--text); }
button { font: inherit; color: inherit; cursor: pointer; }
input, textarea { font: inherit; color: inherit; }
mark { background: color-mix(in srgb, var(--accent) 38%, transparent); color: var(--text); border-radius: 3px; padding: 0 1px; }
.ico { width: 14px; height: 14px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.board { max-width: 1500px; margin: 0 auto; padding: 32px 28px 80px; }
.board h1 { margin: 0 0 6px; font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.board .lede { max-width: 760px; margin: 0 0 22px; color: var(--text-2); font-size: 14px; text-wrap: pretty; }
.controls { display: flex; flex-wrap: wrap; gap: 14px 28px; margin-bottom: 18px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); }
.control { display: flex; flex-direction: column; gap: 6px; }
.control-name { color: var(--text-3); font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.picked { margin-left: 4px; color: var(--accent-text); font-size: 10.5px; }
.frame-label { display: flex; align-items: baseline; gap: 10px; margin: 30px 0 10px; font-size: 15px; font-weight: 600; }
.frame-label span { color: var(--text-3); font-size: 12.5px; font-weight: 400; }
.stage { position: relative; overflow: hidden; border: 1px solid var(--border-2); border-radius: 12px; }
.stage > .desk { transform-origin: 0 0; }
.phones { display: flex; flex-wrap: wrap; gap: 28px; }
.phone-wrap { display: flex; flex-direction: column; gap: 8px; }
.phone-wrap .cap { color: var(--text-3); font-size: 12.5px; }
.notes { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 34px; }
.note-card { padding: 16px 18px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); color: var(--text-2); font-size: 13px; }
.note-card h3 { margin: 0 0 8px; color: var(--text); font-size: 14px; font-weight: 600; }
.note-card p { margin: 0 0 8px; text-wrap: pretty; }
.note-card ul { margin: 0; padding-left: 18px; }
.note-card li { margin-bottom: 5px; }
.segmented { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--border-2); border-radius: 7px; }
.segmented.wrap { flex-wrap: wrap; }
.seg { padding: 3px 10px; border: 0; border-radius: 5px; background: none; color: var(--text-3); font-size: 12px; white-space: nowrap; }
.seg:hover { color: var(--text); }
.seg.on { background: var(--sel); color: var(--text); }
.seg .count { margin-left: 5px; color: var(--text-4); font-variant-numeric: tabular-nums; }
.btn { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 11px; border: 1px solid var(--border-2); border-radius: 7px; background: transparent; color: var(--text-2); font-size: 12.5px; white-space: nowrap; }
.btn:hover { background: var(--hover); color: var(--text); }
.btn.primary { border-color: var(--accent); background: var(--accent-dim); color: var(--accent-text); }
.btn.primary:hover { background: var(--accent); color: #fff; }
.btn.danger { border-color: color-mix(in oklab, #df8b7e 55%, var(--border-2)); color: #df8b7e; }
.btn.big { height: 44px; padding: 0 16px; font-size: 14px; border-radius: 10px; }
.icon-btn { display: grid; place-items: center; width: 28px; height: 28px; border: 0; border-radius: 7px; background: none; color: var(--text-3); }
.icon-btn:hover { background: var(--hover); color: var(--text); }
.section-heading { margin: 0; color: var(--text-3); font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.textin { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 10px; border: 1px solid var(--border-2); border-radius: 8px; background: var(--bg-2); color: var(--text-3); }
.textin:focus-within { border-color: var(--accent); }
.textin input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; color: var(--text); }
.textin input::placeholder { color: var(--text-4); }
.textin .key { padding: 0 5px; border: 1px solid var(--border-2); border-radius: 4px; color: var(--text-4); font-family: var(--mono); font-size: 10.5px; }
.desk { position: relative; display: grid; grid-template-columns: 236px minmax(0, 1fr) auto; grid-template-rows: minmax(0, 1fr); width: 1440px; height: 900px; overflow: hidden; background: var(--bg); }
.nav { display: flex; flex-direction: column; gap: 2px; padding: 12px 10px; border-right: 1px solid var(--border); background: var(--side); }
.nav-brand { display: flex; align-items: center; gap: 10px; height: 24px; margin: 0 8px 18px; font-size: 14px; font-weight: 600; }
.nav-brand i { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 5px; background: #2a7de1; color: #fff; font-size: 11px; font-style: normal; }
.nav-head { margin: 14px 8px 6px; color: var(--text-3); font-size: 12px; }
.nav-item { display: flex; align-items: center; gap: 10px; height: 31px; padding: 0 8px; border-radius: 7px; color: var(--text-2); }
.nav-item .sq { width: 6px; height: 6px; margin: 0 3px; border-radius: 50%; background: var(--border-3); }
.nav-item.on { background: var(--sel); color: var(--text); }
.nav-item .n { margin-left: auto; color: var(--text-3); font-size: 12px; }
.nav-item .n.needs { min-width: 18px; padding: 0 5px; border-radius: 99px; background: var(--needs-bg); color: var(--needs-text); text-align: center; }
.main { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.top { display: flex; align-items: center; gap: 10px; height: 47px; padding: 0 20px; border-bottom: 1px solid var(--border); color: var(--text-2); }
.top b { color: var(--text); font-weight: 600; }
.top .crumb-sep { color: var(--text-4); }
.top .grow { flex: 1; }
.working { display: flex; align-items: center; gap: 10px; height: 52px; padding: 0 20px; border-bottom: 1px solid var(--border); }
.working .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
.working b { font-weight: 600; }
.working u { color: var(--text-2); text-decoration-color: var(--text-4); text-underline-offset: 3px; }
.bar { display: flex; align-items: center; gap: 12px; height: 44px; padding: 0 14px 0 22px; border-bottom: 1px solid var(--border); }
.bar .search { flex: 0 1 460px; }
.bar .sep { width: 1px; height: 16px; background: var(--border-2); }
.bar .flat { color: var(--text-2); }
.bar .flat:hover { color: var(--text); }
.bar .grow { flex: 1; }
.activity { width: 290px; height: 100%; padding: 0 18px; border-left: 1px solid var(--border); background: var(--side); }
.activity-head { display: flex; align-items: center; height: 47px; font-weight: 500; }
.activity-item { margin: 10px 0 14px; color: var(--text-2); }
.activity-item small { display: block; color: var(--text-4); font-size: 11.5px; }
.docs { display: grid; flex: 1; grid-template-rows: minmax(0, 1fr); min-height: 0; overflow: hidden; }
.docs.split { grid-template-columns: minmax(340px, 0.82fr) minmax(0, 1.18fr); }
.list { min-height: 0; overflow-y: auto; padding: 0 10px 40px 8px; }
.split .list { border-right: 1px solid var(--border); }
.shelves { overflow-x: auto; padding: 12px 0 4px 14px; scrollbar-width: none; mask-image: linear-gradient(90deg, #000 calc(100% - 36px), transparent); }
.shelves::-webkit-scrollbar { display: none; }
.group { margin-top: 14px; }
.group-head { position: sticky; top: 0; z-index: 1; display: flex; align-items: baseline; gap: 8px; padding: 8px 14px 6px; background: var(--bg); }
.group-head .gcount { color: var(--text-4); font-size: 12px; font-weight: 500; }
.group.waiting .section-heading { color: var(--needs-text); }
.group.waiting .gcount { color: color-mix(in srgb, var(--needs-text) 70%, var(--text-4)); }
.group-why { margin: -2px 14px 6px; color: var(--text-4); font-size: 12px; }
.row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 14px; width: 100%; padding: 12px 14px; border: 1px solid transparent; border-radius: 10px; background: none; color: var(--text); text-align: left; transition: background 0.15s, border-color 0.15s; }
.row:hover, .row:focus-visible { border-color: var(--border); background: var(--hover); outline: none; }
.row.selected { border-color: var(--border-2); background: var(--sel); }
.row.selected .sheet, .row:hover .sheet { stroke: var(--accent); }
.row.replaced { opacity: 0.55; }
.row-main { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.row-title { font-size: 14px; font-weight: 550; line-height: 1.35; }
.row-summary { display: -webkit-box; overflow: hidden; color: var(--text-3); font-size: 12.5px; line-height: 1.5; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.row-where { margin-right: 6px; color: var(--text-2); font-weight: 500; }
.row-facts { display: flex; flex-wrap: wrap; gap: 0 6px; color: var(--text-4); font-size: 11.5px; }
.row-age { padding-top: 2px; color: var(--text-3); font-size: 11.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.compact .row { align-items: center; padding: 8px 12px; }
.compact .row .thumb { width: 22px; height: 28px; }
.compact .row-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.compact .row-title .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pill { display: inline-flex; align-items: center; gap: 5px; margin-left: 8px; padding: 0 8px; border-radius: 99px; font-size: 11px; font-weight: 500; line-height: 19px; vertical-align: 1px; white-space: nowrap; }
.row .pill { display: flex; width: max-content; margin: 4px 0 1px; }
.compact .row .pill { margin: 0; }
.pill.needs { border: 1px solid var(--needs-line); background: var(--needs-bg); color: var(--needs-text); }
.pill.needs::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--blocking); }
.pill.writing, .pill.replaced { border: 1px dashed var(--border-3); color: var(--text-3); }
.pill.answered { border: 1px solid var(--border-2); color: var(--text-2); }
.thumb { flex: none; overflow: visible; }
.thumb .sheet { fill: var(--raised); stroke: var(--text-4); stroke-width: 1.1; transition: stroke 0.15s; }
.thumb .fold { fill: var(--border-2); stroke: var(--text-4); stroke-width: 1.1; stroke-linejoin: round; }
.thumb .ln { fill: var(--text-4); }
.thumb.draft .sheet { stroke-dasharray: 2.6 2.2; }
.thumb.needs .sheet, .row.selected .thumb.needs .sheet, .row:hover .thumb.needs .sheet { stroke: var(--blocking); }
.thumb .flag { fill: var(--blocking); }
.thumb .fresh { fill: var(--accent); }
.none { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; margin: 8px 14px; color: var(--text-3); }
.none .acts { display: flex; gap: 8px; }
.preview { position: relative; min-height: 0; overflow-y: auto; background: var(--bg); }
.pv { max-width: 720px; padding: 18px 32px 60px; }
.pv-top { position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 10px; margin: -18px -32px 0; padding: 12px 32px 10px; background: color-mix(in srgb, var(--bg) 92%, transparent); backdrop-filter: blur(6px); }
.pv-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; flex: 1; min-width: 0; color: var(--text-3); font-size: 12px; }
.pv-meta .num { color: var(--accent-text); font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; }
.pv-meta .st.needs { color: var(--needs-text); }
.pv-title { margin: 4px 0 6px; font-size: 21px; font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; text-wrap: pretty; }
.pv-facts { margin: 0 0 18px; color: var(--text-4); font-size: 12px; }
.pv-body { color: var(--text-2); font-size: 13.5px; line-height: 1.6; }
.pv-body p { margin: 0 0 10px; text-wrap: pretty; }
.pv-body h2 { margin: 22px 0 6px; color: var(--text); font-size: 15px; font-weight: 600; }
.pv-body ul { margin: 0 0 10px; padding-left: 18px; }
.pv-body li { margin-bottom: 4px; }
.pv-body strong { color: var(--text); font-weight: 600; }
.pv-writing { display: flex; align-items: center; gap: 10px; margin: 0 0 18px; padding: 10px 12px; border: 1px dashed var(--border-3); border-radius: 10px; color: var(--text-3); font-size: 12.5px; }
.pv-end { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 26px; padding-top: 14px; border-top: 1px solid var(--border); color: var(--text-4); font-size: 12px; }
.pv-end .grow { flex: 1; }
kbd { padding: 0 4px; border: 1px solid var(--border-2); border-radius: 4px; color: var(--text-3); font-family: var(--mono); font-size: 10.5px; }
.pv-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; height: 100%; padding: 24px; text-align: center; }
.pv-empty .ghost { display: flex; gap: 10px; margin-bottom: 10px; opacity: 0.7; }
.pv-empty h3 { margin: 0; color: var(--text-2); font-size: 14px; font-weight: 500; }
.pv-empty p { max-width: 320px; margin: 0; color: var(--text-4); font-size: 12.5px; line-height: 1.5; text-wrap: pretty; }
.pv-empty .waiting-link { margin-top: 8px; }
.answer-card { display: flex; flex-direction: column; gap: 10px; margin: 0 0 22px; padding: 14px; border: 1px solid var(--needs-line); border-radius: 12px; background: color-mix(in srgb, var(--blocking) 6%, var(--raised)); }
.answer-card.done { border-color: var(--border-2); background: var(--raised); }
.ac-kind { display: flex; align-items: center; gap: 7px; color: var(--needs-text); font-size: 11.5px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.ac-kind::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--blocking); }
.done .ac-kind { color: var(--text-3); }
.done .ac-kind::before { background: var(--text-4); }
.ac-ask { margin: 0; color: var(--text); font-size: 14.5px; font-weight: 500; }
.ac-answers { display: flex; flex-direction: column; gap: 8px; padding: 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-2); }
.ac-buttons { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; }
.answer { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; width: 100%; padding: 10px 12px; border: 1px solid var(--border-2); border-radius: 9px; background: var(--bg); color: var(--text); text-align: left; }
.answer.first { border-color: var(--accent); }
.answer:hover { border-color: var(--accent); background: var(--hover); }
.answer-label { display: inline-flex; align-items: center; gap: 7px; font-weight: 500; }
.answer-note, .ac-note { margin: 0; color: var(--text-3); font-size: 12px; }
.answer.own { grid-column: 1 / -1; flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; border-style: dashed; border-color: var(--border-3); }
.answer.own .answer-label { align-self: center; }
.answer.own:hover { border-color: var(--accent); border-style: solid; }
.own-box { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 8px; }
.own-box textarea { min-height: 76px; padding: 9px 11px; border: 1px solid var(--accent); border-radius: 9px; outline: 0; background: var(--bg); color: var(--text); resize: vertical; }
.own-box .acts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ac-chosen { display: flex; flex-direction: column; gap: 4px; }
.ac-chosen b { color: var(--text); font-weight: 500; }
.scrim { position: absolute; inset: 0; z-index: 20; background: rgba(5, 6, 8, 0.62); backdrop-filter: blur(2px); }
.full { position: absolute; top: 0; right: 0; bottom: 0; z-index: 21; width: min(980px, 82%); overflow-y: auto; border-left: 1px solid var(--border); background: var(--bg); box-shadow: -24px 0 60px rgba(0, 0, 0, 0.45); }
.full-in { max-width: 800px; margin: 0 auto; padding: 30px 40px 60px; }
.full-head { display: flex; align-items: center; gap: 8px; color: var(--text-3); font-size: 12px; }
.full-head .num { color: var(--accent-text); letter-spacing: 0.04em; text-transform: uppercase; }
.full-head .grow { flex: 1; }
.full-title { margin: 12px 0 10px; font-size: 20px; font-weight: 600; }
.parts-chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px; border: 1px solid var(--border-2); border-radius: 7px; color: var(--text-2); font-size: 12px; }
.full-acts { display: flex; flex-wrap: wrap; gap: 6px; margin: 16px -40px 22px; padding: 12px 40px 0; border-top: 1px solid var(--border); }
.full-acts .grow { flex: 1; }
.phone { position: relative; display: flex; flex-direction: column; width: 390px; height: 844px; overflow: hidden; border: 1px solid var(--border-2); border-radius: 34px; background: var(--bg); }
.ph-status { display: flex; justify-content: space-between; height: 44px; padding: 14px 28px 0; color: var(--text); font-size: 13px; font-weight: 600; }
.ph-top { display: flex; align-items: center; gap: 10px; height: 46px; padding: 0 12px; border-bottom: 1px solid var(--border); color: var(--text-2); }
.ph-top .grow { flex: 1; }
.ph-top b { color: var(--text); font-weight: 600; }
.ph-bar { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); }
.ph-bar .textin { flex: 1; height: 40px; }
.ph-bar .flat { padding: 0 6px; color: var(--text-2); line-height: 40px; }
.ph-bar .btn.primary { width: 40px; height: 40px; justify-content: center; padding: 0; }
.phone .list { flex: 1; padding: 0 6px 30px 0; }
.phone .shelves { padding-left: 10px; }
.phone .seg { padding: 8px 12px; font-size: 13px; }
.phone .group-head, .phone .group-why { padding-left: 10px; margin-left: 0; }
.phone .row { min-height: 44px; padding: 12px 10px; }
.ph-doc { position: absolute; inset: 0; z-index: 5; display: flex; flex-direction: column; background: var(--bg); }
.ph-doc-top { display: flex; align-items: center; gap: 6px; height: 50px; padding: 0 8px; border-bottom: 1px solid var(--border); }
.ph-doc-top .back { display: flex; align-items: center; gap: 4px; height: 44px; padding: 0 8px; border: 0; background: none; color: var(--accent-text); font-size: 15px; }
.ph-doc-top .grow { flex: 1; }
.ph-doc-top .icon-btn { width: 44px; height: 44px; }
.ph-doc .pv { flex: 1; overflow-y: auto; padding: 14px 18px 40px; }
.ph-doc .pv-title { font-size: 20px; }
.ph-doc .ac-buttons { grid-template-columns: 1fr; }
.ph-doc .answer { min-height: 52px; padding: 11px 14px; }
.ph-doc .answer-label { font-size: 14.5px; }
.ph-doc .own-box .btn { height: 44px; padding: 0 16px; font-size: 14px; }
.ph-doc .pv-body { font-size: 15px; }
</style>
</head>
<body>
<main class="board">
    <h1>Documents: the list on the left, a preview on the right</h1>
    <p class="lede">Click a document to read it beside the list; <strong>Open in full</strong> opens today's side panel. Documents that wait on you sit at the top and say what they need. On a phone the list stays alone and a tap opens the document. Everything here is clickable; nothing is saved.</p>

    <div class="controls">
        <div class="control"><span class="control-name">Show the state</span><div class="segmented wrap" id="c-state"></div></div>
        <div class="control"><span class="control-name">Option 1: how a row reads</span><div class="segmented" id="c-rows"></div></div>
        <div class="control"><span class="control-name">Option 2: where waiting documents go</span><div class="segmented" id="c-wait"></div></div>
        <div class="control"><span class="control-name">Activity panel</span><div class="segmented" id="c-activity"></div></div>
    </div>

    <div class="frame-label" data-screen-label="Desktop">Desktop, 1440 wide <span>Try ↑ ↓ to move through the list, Enter to open in full, Esc to go back, / to search.</span></div>
    <div class="stage" id="desk-stage"></div>

    <div class="frame-label" data-screen-label="Phone">Phone, 390 wide <span>The list alone; a tap opens the document in full.</span></div>
    <div class="phones">
        <div class="phone-wrap"><span class="cap">The list, waiting documents on top</span><div id="phone-list"></div></div>
        <div class="phone-wrap"><span class="cap">A document with choice buttons, opened</span><div id="phone-doc"></div></div>
        <div class="phone-wrap"><span class="cap">An empty search</span><div id="phone-empty"></div></div>
    </div>

    <section class="notes">
        <div class="note-card">
            <h3>What I picked, and why</h3>
            <p><strong>1a, rows with a summary.</strong> The summary is what tells two similar titles apart, and the preview column is wide enough to keep it. 1b fits twice the rows, but the list then reads like a file browser and search hits lose the line that shows where the words were found.</p>
            <p><strong>2a, waiting documents on top.</strong> A document that needs you should be the first thing on the page, not a tab you have to think of opening. 2b keeps the age groups pure, but a waiting document from last month sinks out of sight.</p>
        </div>
        <div class="note-card">
            <h3>What a draft means now</h3>
            <ul>
                <li><strong>Your answer is needed</strong> or <strong>Your approval is needed</strong>: amber, with an amber dot on the page icon and an amber count by Documents in the side list. These are drafts until you answer.</li>
                <li><strong>Still being written</strong>: a dashed grey label. A draft, but nothing is asked of you.</li>
                <li><strong>Final</strong>: no label at all, so the labels that remain mean something.</li>
                <li>Answering moves the document out of Waiting on you and makes it final.</li>
            </ul>
        </div>
        <div class="note-card">
            <h3>Widths</h3>
            <ul>
                <li>The preview shows when the page itself is at least 900 wide, side lists included. With Activity shown at 1440 the page is about 914 wide, so the preview just fits; try it with the Activity control.</li>
                <li>Narrower than that, the list is alone and a click opens the side panel, as today.</li>
                <li>On a phone, a tap opens the document full screen; the edit, share, comment, close and delete actions sit in its ⋯ menu, as in the phone inspector today.</li>
            </ul>
        </div>
    </section>
</main>
<script>
const SHELVES = [
    {key: "design", title: "Design work"},
    {key: "plans", title: "Plans"},
    {key: "kanban", title: "Kanban and agentic organization"},
];

const DOCS = [
    {
        n: 76,
        title: "Settings page: which layout to build",
        hours: 3,
        shelf: "design",
        state: "answer",
        links: 2,
        files: 0,
        abstract:
            "The Settings page redesign is drawn three ways in Claude Design, each at desktop and phone width. Pick the one to build and the work starts on it; the other two are kept in the file for later.",
        ask: "Which layout should be built?",
        buttons: [
            {label: "Build 1a, one scrolling page", say: "Build layout 1a, the one scrolling page"},
            {label: "Build 1b, one group at a time", say: "Build layout 1b, one group at a time"},
            {label: "Build 1c, one entry per feature", say: "Build layout 1c, one entry per feature"},
        ],
        parts: [
            {
                h: "What the three layouts are",
                html: "<ul><li><strong>1a, one scrolling page:</strong> every group in one column, with a list of groups on the left that jumps to each. Search narrows the same page.</li><li><strong>1b, one group at a time:</strong> the left list picks a group and only that group shows. Shorter pages, but a setting is harder to find by scrolling.</li><li><strong>1c, one entry per feature:</strong> the left list has a line for every feature, more than forty. Easy to scan, long to scroll.</li></ul>",
            },
            {
                h: "What changes for you",
                html: "<p>Timing settings such as <strong>Runs every 5 tool calls</strong> become a chip you click to change. Changed settings carry a dot and a Reset. Search keeps the matching rows inside their groups.</p>",
            },
            {
                h: "What stays the same",
                html: "<p>Every setting keeps its place and its meaning. Nothing is switched on or off by the redesign itself.</p>",
            },
        ],
    },
    {
        n: 75,
        title: "Overnight refactor plan",
        hours: 9,
        shelf: "plans",
        state: "approve",
        links: 4,
        files: 1,
        abstract:
            "The codebase cleaned and made single-purpose by morning, on a branch of its own. Nothing reaches main until you approve the pull request.",
        ask: "Read the plan, then approve it or say what to change.",
        buttons: [{label: "Approve the plan", say: "The overnight refactor plan is approved; start it"}],
        parts: [
            {
                h: "What gets done",
                html: "<ul><li>Every page reads its rows through the one client.</li><li>Single-purpose helpers replace the three that each did two jobs.</li><li>The test suite runs in under a minute.</li></ul>",
            },
            {
                h: "What does not change",
                html: "<p>No feature is removed and no setting moves. The viewer looks the same.</p>",
            },
            {
                h: "How you check it in the morning",
                html: "<p>One pull request with a list of what moved and why. The boot check must pass before it is opened.</p>",
            },
        ],
    },
    {
        n: 74,
        title: "The demo's three scenarios",
        hours: 26,
        shelf: "",
        state: "writing",
        links: 3,
        files: 0,
        abstract:
            "Every scenario is a real session recorded through the journal. The visitor plays the user: each message waits until they press Send, each question until they choose.",
        parts: [
            {h: "Asking for a plan", html: "<p>A website is asked for and the plan is written phase by phase. One question changes what gets built.</p>"},
            {h: "Asking for a report", html: "<p>Why is an invoice a cent off? The report is written part by part.</p>"},
        ],
    },
    {
        n: 73,
        title: "Plan 25 in short",
        hours: 30,
        shelf: "plans",
        state: "final",
        links: 4,
        files: 0,
        abstract: "Seven phases on one page: what each phase delivers, what it waits on, and how far it got.",
        parts: [
            {h: "The seven phases", html: "<p>Personality profiles, the inspector, helper rows, the boot check, the split tests, the release, the tidy-up.</p>"},
        ],
    },
    {
        n: 72,
        title: "Critic rounds with one command",
        hours: 96,
        shelf: "design",
        state: "final",
        links: 4,
        files: 0,
        abstract:
            "A design critique round becomes one command: the critics are briefed, each opens the app through its own lens, and their findings land in one report.",
        parts: [{h: "How a round runs", html: "<p>The round names its critics, writes each a brief, and waits for all of them before the designer reads the report.</p>"}],
    },
    {
        n: 71,
        title: "Dispatching a helper on another provider",
        hours: 120,
        shelf: "",
        state: "final",
        links: 2,
        files: 0,
        abstract: "A helper agent on Codex or Claude takes one bounded job in a place of its own, hidden from your lists, and brings the work back.",
        parts: [{h: "What a helper may do", html: "<p>Read and write in its own worktree only. It never commits to main.</p>"}],
    },
    {
        n: 69,
        title: "How the engine reloads itself",
        hours: 9 * 24,
        shelf: "",
        state: "final",
        links: 1,
        files: 0,
        abstract: "The server runs the engines; an upgrade is the restart. Nothing has to be stopped by hand.",
        parts: [{h: "When it reloads", html: "<p>After every upgrade, and when a file the engine reads has changed.</p>"}],
    },
    {
        n: 66,
        title: "Release notes for 2.250",
        hours: 12 * 24,
        shelf: "",
        state: "final",
        links: 0,
        files: 2,
        abstract: "Helpers show their job in two lines, the first-run tour waits for open dialogs, and menus open above dialogs.",
        parts: [{h: "What is new", html: "<ul><li>Helper rows in the Helpers menu.</li><li>The first-run tour waits.</li></ul>"}],
    },
    {
        n: 63,
        title: "Settings page sketch",
        hours: 20 * 24,
        shelf: "design",
        state: "replaced",
        by: 76,
        links: 1,
        files: 0,
        abstract: "The first sketch of the Settings page, one long list of switches.",
        parts: [{h: "The sketch", html: "<p>One long list of switches, grouped by feature.</p>"}],
    },
    {
        n: 58,
        title: "Why every resource has one detail panel",
        hours: 34 * 24,
        shelf: "",
        state: "final",
        links: 3,
        files: 0,
        abstract: "A resource's detail panel is one component, shown on its own page and in Home's side panel alike.",
        parts: [{h: "The rule", html: "<p>One panel, two places. A change to the panel shows in both.</p>"}],
    },
    {
        n: 51,
        title: "Tests: three generated runs",
        hours: 41 * 24,
        shelf: "",
        state: "final",
        links: 2,
        files: 0,
        abstract: "The default commands carry no hand-written tests. Three generated runs cover every type and every action.",
        parts: [{h: "The three runs", html: "<p>Every action, the gate, and the boot check.</p>"}],
    },
    {
        n: 47,
        title: "Kanban boards for agent work",
        hours: 60 * 24,
        shelf: "kanban",
        state: "final",
        links: 5,
        files: 1,
        abstract: "Tickets move across stages on a board; an agent picks up the next ticket when it is free.",
        parts: [{h: "The stages", html: "<p>To do, Doing, Needs you, Done.</p>"}],
    },
    {
        n: 45,
        title: "Feedback on the board from a team lead",
        hours: 62 * 24,
        shelf: "kanban",
        state: "final",
        links: 1,
        files: 0,
        abstract: "Cards should show who holds them, and a stage should say how many cards it may hold.",
        parts: [{h: "What was asked", html: "<p>Owners on cards, and a limit per stage.</p>"}],
    },
];

const svg = (body, cls = "ico") => `<svg class="${cls}" viewBox="0 0 16 16" aria-hidden="true">${body}</svg>`;
const ICON = {
    search: svg('<circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5 14 14"/>'),
    plus: svg('<path d="M8 3v10M3 8h10"/>'),
    panel: svg('<rect x="2" y="2.5" width="12" height="11" rx="2"/><path d="M9.5 2.5v11M5 6.5 6.8 8 5 9.5"/>'),
    x: svg('<path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/>'),
    back: svg('<path d="M10 3.5 5.5 8l4.5 4.5"/>'),
    more: svg('<circle cx="3.5" cy="8" r=".9"/><circle cx="8" cy="8" r=".9"/><circle cx="12.5" cy="8" r=".9"/>'),
    save: svg('<path d="M8 2.5v8M4.5 7 8 10.5 11.5 7M3 13.5h10"/>'),
    words: svg('<path d="M3 4.5C3 3.7 3.7 3 4.5 3h7c.8 0 1.5.7 1.5 1.5v5c0 .8-.7 1.5-1.5 1.5H7.5L4.5 13.5V11C3.7 11 3 10.3 3 9.5z"/>'),
    doc: svg('<path d="M4.5 2h5L12 4.5V13a1 1 0 0 1-1 1H4.5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M6 8h4M6 10.5h3"/>'),
};

const esc = (t) => String(t).replace(/[&<>"]/g, (c) => ({"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;"})[c]);
const plain = (html) => html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
const counted = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
const age = (h) => (h < 24 ? `${h}h` : `${Math.round(h / 24)}d`);
const changedWords = (h) => (h < 24 ? `changed ${counted(h, "hour")} ago` : `changed ${counted(Math.round(h / 24), "day")} ago`);
const ageGroup = (h) => (h < 168 ? "Last 7 days" : h < 720 ? "Last 30 days" : "Older");
const shelfTitle = (k) => SHELVES.find((s) => s.key === k)?.title || "";
const byN = (n) => DOCS.find((d) => d.n === n);
const terms = (q) => q.toLowerCase().split(/\s+/).filter(Boolean);

function marked(text, words) {
    let out = esc(text);
    for (const w of words) out = out.replace(new RegExp(`(${w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"), "<mark>$1</mark>");
    return out;
}

function markedHtml(html, words) {
    if (!words.length) return html;
    return html.replace(/>([^<]+)</g, (m, t) => `>${marked(t, words).replace(/&amp;/g, "&")}<`);
}

function found(d, words) {
    if (!words.length) return null;
    const has = (t) => words.every((w) => t.toLowerCase().includes(w));
    const all = [d.title, d.abstract, ...d.parts.map((p) => p.h + " " + plain(p.html))].join(" ");
    if (!has(all)) return null;
    if (has(d.title)) return {score: 8, where: "", text: d.abstract};
    if (has(d.abstract)) return {score: 3, where: "", text: d.abstract};
    const part = d.parts.find((p) => has(p.h + " " + plain(p.html))) || d.parts[0];
    const flat = plain(part.html);
    const at = Math.max(0, flat.toLowerCase().indexOf(words[0]) - 50);
    return {score: 1, where: part.h, text: (at ? "…" : "") + flat.slice(at, at + 160)};
}

const OPT = {rows: "1a", wait: "2a", activity: false};
const instances = [];
let message = 16480;

class Journal {
    constructor(el, mode, preset = {}) {
        this.el = el;
        this.mode = mode;
        this.s = {selected: null, query: "", order: "recent", shelf: "", full: false, answered: {}, own: 0, ownText: "", ...preset};
        instances.push(this);
        this.mount();
        el.addEventListener("click", (e) => this.click(e));
        el.addEventListener("input", (e) => this.input(e));
    }

    state(d) {
        return this.s.answered[d.n] ? "answered" : d.state;
    }

    needs(d) {
        return ["answer", "approve"].includes(this.state(d));
    }

    draft(d) {
        return ["answer", "approve", "writing"].includes(this.state(d));
    }

    waitingCount() {
        return DOCS.filter((d) => this.needs(d)).length;
    }

    pill(d) {
        const st = this.state(d);
        if (st === "answer") return '<span class="pill needs">Your answer is needed</span>';
        if (st === "approve") return '<span class="pill needs">Your approval is needed</span>';
        if (st === "writing") return '<span class="pill writing">Still being written</span>';
        if (st === "replaced") return `<span class="pill replaced">Replaced by doc ${d.by}</span>`;
        return "";
    }

    standing(d) {
        const st = this.state(d);
        return {
            answer: ["needs", "Draft until you answer"],
            approve: ["needs", "Draft until you approve it"],
            writing: ["", "Draft while it is still being written"],
            replaced: ["", `Replaced by doc ${d.by}`],
            answered: ["", "Final"],
            final: ["", "Final"],
        }[st];
    }

    thumb(d) {
        const lines = [16, 12, 15, 9, 14, 11, 13].slice(0, Math.min(Math.max(d.parts.length + 2, 1), 7));
        const cls = ["thumb", this.draft(d) ? "draft" : "", this.needs(d) ? "needs" : ""].join(" ");
        const mark = this.needs(d) ? '<circle class="flag" cx="30" cy="3" r="3.4"/>' : d.hours < 24 ? '<circle class="fresh" cx="30" cy="3" r="2.6"/>' : "";
        return `<svg class="${cls}" viewBox="0 0 32 40" width="32" height="40" aria-hidden="true">
            <path class="sheet" d="M5 1h16l9 9v27a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2z"/>
            <path class="fold" d="M21 1v7a2 2 0 0 0 2 2h7z"/>
            ${lines.map((w, i) => `<rect class="ln" x="8" y="${14 + i * 3.4}" width="${w}" height="1.5" rx=".75"/>`).join("")}
            ${d.files ? '<path class="fold" d="M9.5 -1.5v9a2.3 2.3 0 0 0 4.6 0v-7.2a1.5 1.5 0 0 0-3 0v6.4" fill="none"/>' : ""}
            ${mark}</svg>`;
    }

    facts(d) {
        return [
            `Doc ${d.n}`,
            counted(d.parts.length, "part"),
            d.files ? counted(d.files, "file") : "",
            d.links ? counted(d.links, "link") : "",
            !this.s.shelf && d.shelf ? `in ${shelfTitle(d.shelf)}` : "",
        ].filter(Boolean);
    }

    row(d, hit) {
        const words = terms(this.s.query);
        const sel = this.s.selected === d.n && this.mode === "desk";
        const cls = ["row", sel ? "selected" : "", this.state(d) === "replaced" ? "replaced" : ""].join(" ");
        if (OPT.rows === "1b" && this.mode === "desk")
            return `<button type="button" class="${cls}" data-open="${d.n}">${this.thumb(d)}
                <span class="row-title"><span class="t">${marked(d.title, words)}</span>${this.pill(d)}</span>
                <span class="row-age">${age(d.hours)}</span></button>`;
        const summary = hit ? hit.text : d.abstract;
        const where = hit?.where ? `<span class="row-where">In “${esc(hit.where)}”:</span>` : "";
        return `<button type="button" class="${cls}" data-open="${d.n}">${this.thumb(d)}
            <span class="row-main">
                <span class="row-title">${marked(d.title, words)}</span>${this.pill(d)}
                <span class="row-summary">${where}${marked(summary, words)}</span>
                <span class="row-facts">${this.facts(d).map(esc).join('<span aria-hidden="true">·</span>')}</span>
            </span>
            <span class="row-age">${age(d.hours)}</span></button>`;
    }

    onShelf() {
        const k = this.s.shelf;
        if (k === "waiting") return DOCS.filter((d) => this.needs(d));
        if (k === "loose") return DOCS.filter((d) => !d.shelf);
        return k ? DOCS.filter((d) => d.shelf === k) : DOCS;
    }

    sorted(list) {
        const replaced = (d) => Number(this.state(d) === "replaced");
        return [...list].sort((a, b) => (this.s.order === "title" ? a.title.localeCompare(b.title) : replaced(a) - replaced(b) || a.hours - b.hours));
    }

    groups() {
        const words = terms(this.s.query);
        const list = this.sorted(this.onShelf());
        if (words.length) {
            const hits = list.map((d) => ({d, hit: found(d, words)})).filter((h) => h.hit).sort((a, b) => b.hit.score - a.hit.score);
            return {search: true, total: list.length, groups: [{title: `${hits.length} of ${counted(list.length, "document")} match`, list: hits}]};
        }
        const groups = [];
        let rest = list;
        if (OPT.wait === "2a" && this.s.shelf !== "waiting") {
            const waiting = list.filter((d) => this.needs(d));
            rest = list.filter((d) => !this.needs(d));
            if (waiting.length) groups.push({title: "Waiting on you", waiting: true, list: waiting.map((d) => ({d}))});
        }
        if (this.s.order === "title") groups.push({title: groups.length ? "Every other document" : "", list: rest.map((d) => ({d}))});
        else
            for (const d of rest) {
                const t = ageGroup(d.hours);
                let g = groups.find((x) => x.title === t);
                if (!g) groups.push((g = {title: t, list: []}));
                g.list.push({d});
            }
        return {search: false, groups: groups.filter((g) => g.list.length)};
    }

    visible() {
        return this.groups().groups.flatMap((g) => g.list.map((h) => h.d.n));
    }

    shelvesHtml() {
        const opts = [{key: "", label: "All", count: DOCS.length}];
        if (OPT.wait === "2b") opts.push({key: "waiting", label: "Waiting on you", count: this.waitingCount()});
        for (const s of SHELVES) opts.push({key: s.key, label: s.title, count: DOCS.filter((d) => d.shelf === s.key).length});
        opts.push({key: "loose", label: "Not in a collection", count: DOCS.filter((d) => !d.shelf).length});
        return `<nav class="shelves" aria-label="Collections"><div class="segmented">${opts
            .map((o) => `<button type="button" class="seg ${o.key === this.s.shelf ? "on" : ""}" data-shelf="${o.key}">${esc(o.label)}<span class="count">${o.count}</span></button>`)
            .join("")}</div></nav>`;
    }

    listHtml() {
        const {search, groups} = this.groups();
        const q = this.s.query.trim();
        let body = "";
        for (const g of groups) {
            const head = g.title
                ? `<div class="group-head"><h3 class="section-heading">${esc(g.title)}</h3>${g.waiting ? `<span class="gcount">${g.list.length}</span>` : ""}</div>`
                : "";
            const why = g.waiting ? '<p class="group-why">The agent waits for your answer or approval before it goes on.</p>' : "";
            const rows = g.list.length || !search ? g.list.map((h) => this.row(h.d, h.hit)).join("") : this.noneHtml(q);
            body += `<section class="group ${g.waiting ? "waiting" : ""}">${head}${why}${rows}</section>`;
        }
        return this.shelvesHtml() + body;
    }

    noneHtml(q) {
        const inShelf = this.s.shelf && this.s.shelf !== "";
        return `<div class="none">Nothing in ${inShelf ? "this collection" : "the documents"} mentions “${esc(q)}”.
            <div class="acts"><button type="button" class="btn" data-clear>Clear the search</button>
            ${inShelf ? '<button type="button" class="btn" data-shelf="">Search every document</button>' : ""}</div></div>`;
    }

    answerCard(d) {
        if (!d.buttons) return "";
        const a = this.s.answered[d.n];
        if (a) {
            const what = a.own
                ? `You answered in your own words: <b>“${esc(a.own)}”</b>`
                : d.state === "approve"
                  ? "<b>You approved it.</b>"
                  : `You chose <b>${esc(a.label)}</b>.`;
            return `<section class="answer-card done"><span class="ac-kind">Your answer</span>
                <div class="ac-chosen"><span>${what}</span><p class="ac-note">Sent as your message ${a.msg}. The reply comes in the chat. This document is final now.</p></div></section>`;
        }
        const approve = d.state === "approve";
        const own =
            this.s.own === d.n
                ? `<div class="own-box"><textarea data-owntext placeholder="${approve ? "Say what to change, or anything else" : "Write your answer"}" aria-label="Your answer in your own words">${esc(this.s.ownText)}</textarea>
                    <div class="acts"><button type="button" class="btn primary" data-send="${d.n}">Send your answer</button>
                    <button type="button" class="btn" data-cancel>Cancel</button>
                    <span class="ac-note">It goes to the agent as your message about doc ${d.n}.</span></div></div>`
                : `<button type="button" class="answer own" data-own="${d.n}"><span class="answer-label">${ICON.words}Answer in your own words</span>
                    <span class="answer-note">${approve ? "Ask for changes, or say anything else" : "Write what you want instead of choosing"}</span></button>`;
        return `<section class="answer-card" id="answer-${d.n}"><span class="ac-kind">${approve ? "Your approval is needed" : "Your answer is needed"}</span>
            <p class="ac-ask">${esc(d.ask)}</p>
            <div class="ac-answers"><div class="ac-buttons">${d.buttons
                .map(
                    (b, i) => `<button type="button" class="answer ${i === 0 ? "first" : ""}" data-press="${d.n}:${i}">
                    <span class="answer-label">${esc(b.label)}</span><span class="answer-note">Sends as your message: “${esc(b.say)}”</span></button>`
                )
                .join("")}${own}</div>
            <p class="ac-note">${approve ? "The plan stays a draft until you answer." : "Choose one, or answer in your own words. The buttons go away once you answer."}</p></div></section>`;
    }

    bodyHtml(d) {
        const words = terms(this.s.query);
        const writing =
            this.state(d) === "writing"
                ? '<div class="pv-writing">The agent is still writing this document. It becomes final when it is finished; nothing is asked of you.</div>'
                : "";
        const replaced =
            this.state(d) === "replaced"
                ? `<div class="pv-writing">Doc ${d.by} is the newer version. <button type="button" class="btn" data-open="${d.by}">Read doc ${d.by}</button></div>`
                : "";
        return `${writing}${replaced}<div class="pv-body"><p>${marked(d.abstract, words)}</p>${d.parts
            .map((p) => `<h2>${marked(p.h, words)}</h2>${markedHtml(p.html, words)}`)
            .join("")}</div>`;
    }

    metaHtml(d) {
        const [tone, words] = this.standing(d);
        return `<span class="num">Doc ${d.n}</span><span>·</span><span class="st ${tone}">${words}</span><span>·</span><span>${changedWords(d.hours)}</span>${
            d.shelf ? `<span>·</span><span>in ${esc(shelfTitle(d.shelf))}</span>` : ""
        }`;
    }

    previewHtml() {
        const d = byN(this.s.selected);
        const visible = this.visible();
        if (!d || !visible.includes(d.n)) {
            if (terms(this.s.query).length && !visible.length)
                return `<div class="pv-empty"><h3>No document to show</h3><p>Nothing matches the search, so there is nothing to read here. Clear the search to see every document again.</p></div>`;
            const waiting = this.waitingCount();
            return `<div class="pv-empty"><div class="ghost">${this.thumb(DOCS[3])}</div><h3>Choose a document to read it here</h3>
                <p>Click a document on the left and its text shows here. Open it in full to edit, comment on, share or close it.</p>
                ${
                    waiting
                        ? `<button type="button" class="btn primary waiting-link" data-open="${DOCS.find((x) => this.needs(x)).n}">Read the first of ${counted(waiting, "document")} waiting on you</button>`
                        : ""
                }</div>`;
        }
        return `<article class="pv">
            <div class="pv-top"><div class="pv-meta">${this.metaHtml(d)}</div>
                <button type="button" class="btn primary" data-full>${ICON.panel}Open in full</button>
                <button type="button" class="icon-btn" data-deselect title="Hide the preview" aria-label="Hide the preview">${ICON.x}</button></div>
            <h2 class="pv-title">${marked(d.title, terms(this.s.query))}</h2>
            <p class="pv-facts">${this.facts(d).slice(1).filter((f) => !f.startsWith("in ")).map(esc).join(" · ")}</p>
            ${this.answerCard(d)}${this.bodyHtml(d)}
            <div class="pv-end"><span>This is a preview. Open it in full to edit, comment on, share or close it.</span><span class="grow"></span>
                <span><kbd>↑</kbd> <kbd>↓</kbd> next document · <kbd>Enter</kbd> open in full</span></div></article>`;
    }

    fullHtml() {
        const d = byN(this.s.selected);
        if (!this.s.full || !d) return "";
        return `<div class="scrim" data-unfull></div><aside class="full" aria-label="Document ${d.n}"><div class="full-in">
            <div class="full-head">${ICON.doc}<span class="num">Document ${d.n}</span>${this.pill(d) || '<span class="pill answered">Final</span>'}<span>${age(d.hours).toUpperCase()}</span>
                <span class="grow"></span><button type="button" class="icon-btn" title="Download" aria-label="Download">${ICON.save}</button>
                <button type="button" class="icon-btn" data-unfull title="Back to the list" aria-label="Back to the list">${ICON.x}</button></div>
            <h2 class="full-title">${esc(d.title)}</h2>
            <span class="parts-chip">1 of ${d.parts.length} · ${esc(d.parts[0].h)} →</span>
            <div class="full-acts"><button class="btn">Edit</button><button class="btn">Add to collection</button><button class="btn">Close</button><button class="btn danger">Delete</button>
                <span class="grow"></span><button class="btn">Attach files</button><button class="btn">Share</button><button class="btn">Comments</button></div>
            ${this.answerCard(d)}${this.bodyHtml(d)}</div></aside>`;
    }

    phoneDocHtml() {
        const d = byN(this.s.open);
        if (!d) return "";
        return `<div class="ph-doc"><div class="ph-status"><span>9:41</span><span>5G</span></div><div class="ph-doc-top"><button type="button" class="back" data-back>${ICON.back}Documents</button><span class="grow"></span>
            <button type="button" class="icon-btn" title="Edit, share, comment, close or delete" aria-label="More actions">${ICON.more}</button></div>
            <article class="pv"><div class="pv-meta">${this.metaHtml(d)}</div><h2 class="pv-title">${esc(d.title)}</h2>
            <p class="pv-facts">${this.facts(d).slice(1).filter((f) => !f.startsWith("in ")).map(esc).join(" · ")}</p>${this.answerCard(d)}${this.bodyHtml(d)}</article></div>`;
    }

    mount() {
        if (this.mode === "phone") {
            this.el.innerHTML = `<div class="phone"><div class="ph-status"><span>9:41</span><span>5G</span></div>
                <div class="ph-top"><span class="sq"></span><b>main</b><span class="grow"></span>${ICON.search}</div>
                <div class="ph-bar"><label class="textin">${ICON.search}<input type="search" data-q placeholder="Search titles, text and files" value="${esc(this.s.query)}"></label>
                <a class="flat" href="#">Files</a><button type="button" class="btn primary" aria-label="New document">${ICON.plus}</button></div>
                <div class="list" data-list></div><div data-doc></div></div>`;
        } else {
            this.el.innerHTML = `<div class="desk"><aside class="nav"><div class="nav-brand"><i>A</i>agent-journal</div>
                <div class="nav-item"><span class="sq"></span>Hub</div><div class="nav-head">Environment</div>
                ${["Home", "Suggestions", "Plans", "To-dos", "Collections"].map((t) => `<div class="nav-item"><span class="sq"></span>${t}</div>`).join("")}
                <div class="nav-head">Project</div><div class="nav-item"><span class="sq"></span>Board<span class="n">35</span></div>
                <div class="nav-item on"><span class="sq"></span>Documents<span class="n needs" data-navcount title="Documents waiting on you"></span></div>
                ${["Triggers", "Sequences", "Checks", "Plugins", "Resources", "Settings"].map((t) => `<div class="nav-item"><span class="sq"></span>${t}</div>`).join("")}</aside>
                <div class="main"><div class="top">agent-journal <span class="crumb-sep">/</span> main <span class="crumb-sep">/</span> <b>Documents</b><span class="grow"></span>${ICON.search}</div>
                <div class="working"><span class="dot"></span><b>Working</b><u>to-do 2909 · The test suite runs in seconds again</u></div>
                <div class="bar"><label class="textin search">${ICON.search}<input type="search" data-q placeholder="Search titles, text and files" aria-label="Find a document" value="${esc(this.s.query)}"><span class="key">/</span></label>
                <div class="segmented" data-orderbox></div><span class="sep"></span><a class="flat" href="#">Files</a><span class="grow"></span>
                <button type="button" class="btn primary">${ICON.plus}New document</button></div>
                <div class="docs split"><div class="list" data-list></div><div class="preview" data-preview></div></div></div>
                <div data-activity style="display: contents"></div><div data-full style="display: contents"></div></div>`;
        }
        this.render();
    }

    render() {
        const q = (sel) => this.el.querySelector(sel);
        q("[data-list]").innerHTML = this.listHtml();
        q("[data-list]").classList.toggle("compact", OPT.rows === "1b" && this.mode === "desk");
        if (this.mode === "phone") {
            q("[data-doc]").innerHTML = this.phoneDocHtml();
            return;
        }
        q("[data-preview]").innerHTML = this.previewHtml();
        q("div[data-full]").innerHTML = this.fullHtml();
        q("[data-orderbox]").innerHTML = [
            ["recent", "Newest"],
            ["title", "A to Z"],
        ]
            .map(([k, l]) => `<button type="button" class="seg ${this.s.order === k ? "on" : ""}" data-order="${k}">${l}</button>`)
            .join("");
        const n = this.waitingCount();
        q("[data-navcount]").textContent = n || "";
        q("[data-navcount]").style.display = n ? "" : "none";
        q("[data-activity]").innerHTML = OPT.activity
            ? `<aside class="activity"><div class="activity-head">Activity</div>${["To-do created · 2910", "Message read · 16575", "Updated helper · 81", "Message closed · 16574", "1 agent update"]
                  .map((t) => `<div class="activity-item">${t}<small>Agent · now</small></div>`)
                  .join("")}</aside>`
            : "";
    }

    select(n) {
        this.s.selected = n;
        this.s.own = 0;
        this.render();
        const r = this.el.querySelector(`[data-list] .row[data-open="${n}"]`);
        const list = this.el.querySelector("[data-list]");
        if (r && list) {
            const top = r.offsetTop - list.offsetTop;
            if (top < list.scrollTop + 40 || top + r.offsetHeight > list.scrollTop + list.clientHeight) list.scrollTop = Math.max(0, top - 80);
        }
        const pv = this.el.querySelector("[data-preview]");
        if (pv) pv.scrollTop = 0;
    }

    click(e) {
        const t = e.target.closest("button, a, [data-unfull]");
        if (!t) return;
        if (t.tagName === "A") e.preventDefault();
        const ds = t.dataset;
        if (ds.open) {
            const n = Number(ds.open);
            if (this.mode === "phone") {
                this.s.open = n;
                this.s.own = 0;
                this.render();
            } else this.select(n);
        } else if ("shelf" in ds) {
            this.s.shelf = ds.shelf;
            this.render();
        } else if (ds.order) {
            this.s.order = ds.order;
            this.render();
        } else if ("clear" in ds) {
            this.s.query = "";
            this.el.querySelector("[data-q]").value = "";
            this.render();
        } else if ("full" in ds) {
            this.s.full = true;
            this.render();
        } else if ("unfull" in ds) {
            this.s.full = false;
            this.render();
        } else if ("deselect" in ds) {
            this.s.selected = null;
            this.render();
        } else if ("back" in ds) {
            this.s.open = null;
            this.render();
        } else if (ds.own) {
            this.s.own = Number(ds.own);
            this.s.ownText = "";
            this.render();
            this.el.querySelector("[data-owntext]")?.focus();
        } else if ("cancel" in ds) {
            this.s.own = 0;
            this.render();
        } else if (ds.press) {
            const [n, i] = ds.press.split(":").map(Number);
            this.s.answered[n] = {label: byN(n).buttons[i].label, msg: message++};
            this.render();
        } else if (ds.send) {
            const text = this.s.ownText.trim();
            if (!text) return this.el.querySelector("[data-owntext]")?.focus();
            this.s.answered[Number(ds.send)] = {own: text, msg: message++};
            this.s.own = 0;
            this.render();
        }
    }

    input(e) {
        if (e.target.matches("[data-q]")) {
            this.s.query = e.target.value;
            this.render();
        } else if (e.target.matches("[data-owntext]")) this.s.ownText = e.target.value;
    }

    key(e) {
        if (e.target.matches("input, textarea")) {
            if (e.key === "Escape" && e.target.matches("[data-q]")) {
                e.target.value = "";
                this.s.query = "";
                this.render();
            }
            return;
        }
        if (e.key === "/") {
            e.preventDefault();
            this.el.querySelector("[data-q]").focus();
        } else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
            e.preventDefault();
            const v = this.visible();
            const i = v.indexOf(this.s.selected);
            this.select(v[Math.max(0, Math.min(v.length - 1, i + (e.key === "ArrowDown" ? 1 : -1)))] ?? v[0]);
        } else if (e.key === "Enter" && this.s.selected) {
            this.s.full = true;
            this.render();
        } else if (e.key === "Escape") {
            if (this.s.full) this.s.full = false;
            else this.s.selected = null;
            this.render();
        }
    }
}

const DESK_STATES = {
    none: {selected: null, query: "", full: false},
    draft: {selected: 75, query: "", full: false},
    choice: {selected: 76, query: "", full: false},
    empty: {selected: null, query: "invoice rounding", full: false},
};

function boardSeg(box, options, value, pick) {
    box.innerHTML = options
        .map(([k, l, picked]) => `<button type="button" class="seg ${k === value ? "on" : ""}" data-k="${k}">${l}${picked ? '<span class="picked">picked</span>' : ""}</button>`)
        .join("");
    box.onclick = (e) => {
        const b = e.target.closest("[data-k]");
        if (b) pick(b.dataset.k);
    };
}

function fit() {
    const stage = document.querySelector("#desk-stage");
    const desk = stage.querySelector(".desk");
    const scale = Math.min(1, stage.clientWidth / 1440);
    desk.style.transform = `scale(${scale})`;
    stage.style.height = `${900 * scale}px`;
}

function boot() {
    const desk = new Journal(document.querySelector("#desk-stage"), "desk", DESK_STATES.choice);
    new Journal(document.querySelector("#phone-list"), "phone");
    new Journal(document.querySelector("#phone-doc"), "phone", {open: 76});
    new Journal(document.querySelector("#phone-empty"), "phone", {query: "invoice rounding"});
    let current = "choice";
    const drawControls = () => {
        boardSeg(
            document.querySelector("#c-state"),
            [
                ["none", "Nothing selected"],
                ["draft", "A draft that needs you"],
                ["choice", "A document with choice buttons"],
                ["empty", "An empty search"],
                ["full", "Opened in full"],
            ],
            current,
            (k) => {
                current = k;
                Object.assign(desk.s, k === "full" ? {selected: 76, query: "", full: true} : DESK_STATES[k], {own: 0, answered: {}, shelf: ""});
                desk.el.querySelector("[data-q]").value = desk.s.query;
                desk.render();
                drawControls();
            }
        );
        boardSeg(document.querySelector("#c-rows"), [["1a", "1a · Rows with a summary", true], ["1b", "1b · One-line rows"]], OPT.rows, (k) => {
            OPT.rows = k;
            instances.forEach((i) => i.render());
            drawControls();
        });
        boardSeg(document.querySelector("#c-wait"), [["2a", "2a · Waiting documents on top", true], ["2b", "2b · A Waiting on you tab"]], OPT.wait, (k) => {
            OPT.wait = k;
            instances.forEach((i) => {
                if (i.s.shelf === "waiting") i.s.shelf = "";
                i.render();
            });
            drawControls();
        });
        boardSeg(document.querySelector("#c-activity"), [["off", "Hidden"], ["on", "Shown"]], OPT.activity ? "on" : "off", (k) => {
            OPT.activity = k === "on";
            desk.render();
            drawControls();
        });
    };
    drawControls();
    document.addEventListener("keydown", (e) => desk.key(e));
    window.addEventListener("resize", fit);
    fit();
}

boot();

</script>
</body>
</html>
