<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Areev</title>
<style>
/* ── Console v2 · "cobalt" ────────────────────────────────────────────
Pure white surfaces, one saturated cobalt carrying every action.
Burnt orange = needs attention, red = destructive, teal = content
address. Monospace is reserved for developer mode. Light is
canonical; dark mirrors it token for token. Design source of truth
is the Paper file "Areev", page "Console v2 — Redesign". */
:root {
color-scheme: light;
--bg:#F6F7FB; --surface:#FFFFFF; --raise:#F1F3F8; --well:#F7F8FC;
--line:#E3E6ED; --line2:#D5DAE6; --hair:#EDEFF4;
--ink:#0B0D12; --text:#3E4657; --muted:#5A6072; --dim:#6E7686; --faint:#8B92A3; --ghost:#A8AEBC;
--accent:#1F3BE0; --accent-hover:#1730BC; --accent-soft:#E8ECFF; --accent-line:#C2CCEE;
--ok:#0F8A5F; --ok-soft:#E9F6F0; --ok-line:#CBE8DA;
--warn:#C2410C; --warn-soft:#FFF3EC; --warn-line:#FBDCC8;
--danger:#C81E1E; --danger-soft:#FDECEC; --danger-line:#F6CDCD;
--hash:#0E8F82;
--cat-person:#1F3BE0; --cat-org:#0E8F82; --cat-project:#7A3BE0;
--cat-thing:#B45309; --cat-note:#F2542D;
--sel:rgba(31,59,224,.20);
--shadow:0 10px 30px rgba(11,13,18,.10);
--card-shadow:0 1px 2px rgba(11,13,18,.04), 0 2px 6px rgba(11,13,18,.05);
--sans:'Inter',-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
--mono:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;
/* Layout measures. Widen with the viewport, but never past the point
where a line of prose becomes tiring to read. */
--col:880px; --wide:1500px;
}
@media (min-width:1500px) { :root { --col:1000px; --wide:1700px; } }
@media (min-width:1800px) { :root { --col:1100px; --wide:1900px; } }
@media (min-width:2200px) { :root { --col:1200px; --wide:2100px; } }
@media (prefers-color-scheme: dark) { :root { color-scheme: dark;
--bg:#0B0E14; --surface:#111621; --raise:#171D2A; --well:#0D121B;
--line:#222A38; --line2:#333C4D; --hair:#1C2431;
--ink:#F2F5FA; --text:#C9D1DE; --muted:#9AA4B7; --dim:#8B95A8; --faint:#79839A; --ghost:#5F6980;
--accent:#7C93F5; --accent-hover:#95A9F8; --accent-soft:#1A2340; --accent-line:#39477A;
--ok:#3DD68C; --ok-soft:#12251C; --ok-line:#255140;
--warn:#F59E5B; --warn-soft:#2A1A11; --warn-line:#5A3A22;
--danger:#F87171; --danger-soft:#2B1517; --danger-line:#5E2B2E;
--hash:#3FCFC0;
--cat-person:#7C93F5; --cat-org:#3FCFC0; --cat-project:#B39BFF;
--cat-thing:#E0A458; --cat-note:#FF8A66;
--sel:rgba(124,147,245,.32);
--shadow:0 10px 30px rgba(0,0,0,.45);
--card-shadow:0 1px 2px rgba(0,0,0,.30);
} }
:root[data-theme="light"] { color-scheme: light;
--bg:#F6F7FB; --surface:#FFFFFF; --raise:#F1F3F8; --well:#F7F8FC;
--line:#E3E6ED; --line2:#D5DAE6; --hair:#EDEFF4;
--ink:#0B0D12; --text:#3E4657; --muted:#5A6072; --dim:#6E7686; --faint:#8B92A3; --ghost:#A8AEBC;
--accent:#1F3BE0; --accent-hover:#1730BC; --accent-soft:#E8ECFF; --accent-line:#C2CCEE;
--ok:#0F8A5F; --ok-soft:#E9F6F0; --ok-line:#CBE8DA;
--warn:#C2410C; --warn-soft:#FFF3EC; --warn-line:#FBDCC8;
--danger:#C81E1E; --danger-soft:#FDECEC; --danger-line:#F6CDCD;
--hash:#0E8F82;
--cat-person:#1F3BE0; --cat-org:#0E8F82; --cat-project:#7A3BE0;
--cat-thing:#B45309; --cat-note:#F2542D;
--sel:rgba(31,59,224,.20);
--shadow:0 10px 30px rgba(11,13,18,.10);
--card-shadow:0 1px 2px rgba(11,13,18,.04), 0 2px 6px rgba(11,13,18,.05);
}
:root[data-theme="dark"] { color-scheme: dark;
--bg:#0B0E14; --surface:#111621; --raise:#171D2A; --well:#0D121B;
--line:#222A38; --line2:#333C4D; --hair:#1C2431;
--ink:#F2F5FA; --text:#C9D1DE; --muted:#9AA4B7; --dim:#8B95A8; --faint:#79839A; --ghost:#5F6980;
--accent:#7C93F5; --accent-hover:#95A9F8; --accent-soft:#1A2340; --accent-line:#39477A;
--ok:#3DD68C; --ok-soft:#12251C; --ok-line:#255140;
--warn:#F59E5B; --warn-soft:#2A1A11; --warn-line:#5A3A22;
--danger:#F87171; --danger-soft:#2B1517; --danger-line:#5E2B2E;
--hash:#3FCFC0;
--cat-person:#7C93F5; --cat-org:#3FCFC0; --cat-project:#B39BFF;
--cat-thing:#E0A458; --cat-note:#FF8A66;
--sel:rgba(124,147,245,.32);
--shadow:0 10px 30px rgba(0,0,0,.45);
--card-shadow:0 1px 2px rgba(0,0,0,.30);
}
* { box-sizing:border-box; margin:0; }
html, body { height:100%; }
/* The reading surface is the brightest plane; nav and developer chrome sit
one step back on the tint. (The reverse reads as an unfinished page.) */
body { background:var(--surface); color:var(--text); font:15px/1.55 var(--sans); -webkit-font-smoothing:antialiased; }
a { color:var(--accent); text-decoration:none; }
::-webkit-scrollbar { width:10px; height:10px; }
::-webkit-scrollbar-thumb { background:var(--line2); border-radius:8px; border:3px solid transparent; background-clip:content-box; }
:focus-visible { outline:2px solid var(--accent); outline-offset:2px; border-radius:4px; }
@media (prefers-reduced-motion: reduce) { * { transition:none !important; animation:none !important; } }
/* Developer mode: hidden by default, revealed by html.dev. Explicit
display values (not `revert`, which would drop id-level flex rules). */
/* Belt and braces against stored values that have no wrap opportunities
(JSON, hashes, URLs): flex children must be allowed to shrink, and text
must be allowed to break mid-token. Nothing may scroll the page sideways. */
.mrow .say, .mrow .prov, .srow .t, .evi-row .t, .ev .say, .ans-text,
.kv .v, #ginspect .meta { min-width:0; overflow-wrap:anywhere; }
.mrow, .srow, .evi-row, .ev, .acts { min-width:0; }
.dev-only { display:none !important; }
html.dev .dev-only { display:block !important; }
html.dev .dev-flex { display:flex !important; }
/* ── shell ── */
#shell { display:flex; min-height:100vh; align-items:stretch; }
#rail { width:260px; flex-shrink:0; background:var(--bg); border-right:1px solid var(--line);
padding:26px 18px 20px 22px; display:flex; flex-direction:column; gap:26px; position:sticky; top:0; height:100vh; }
#main { flex:1; min-width:0; display:flex; flex-direction:column; }
.page { padding:44px 56px 56px 48px; display:flex; flex-direction:column; gap:24px; }
/* One reading column, centred in whatever space the rail leaves. A cap
keeps line length sane; auto margins stop it hugging the rail on a wide
monitor, where a left-pinned column just looks like a broken page. */
.col { width:100%; max-width:var(--col); margin-left:auto; margin-right:auto; }
.wide { width:100%; max-width:var(--wide); margin-left:auto; margin-right:auto; }
/* ── query workbench ── */
/* The composer is one surface holding the editor and everything you can do
to it, so Run reads as the act and the rest as afterthoughts. */
/* No overflow:hidden here — the Format menu is absolutely positioned inside
the action bar, and clipping the composer clips the menu. Round the
children instead. */
.composer { background:var(--surface); border:1px solid var(--line2); border-radius:12px;
box-shadow:var(--card-shadow); }
/* The editor is a transparent textarea over a highlighted mirror: the caret,
selection and IME stay native, the colour comes from the layer beneath. */
.qedit { position:relative; }
#qhl, #qtext { font-family:var(--mono); font-size:13px; line-height:21px;
padding:16px 18px; white-space:pre-wrap; overflow-wrap:break-word;
border:0; margin:0; }
#qhl { position:absolute; inset:0; pointer-events:none; overflow:hidden;
color:var(--text); border-radius:12px 12px 0 0; }
#qtext { position:relative; width:100%; display:block; resize:none; overflow-y:auto;
background:none; color:transparent; caret-color:var(--ink);
border-radius:12px 12px 0 0; min-height:88px; max-height:340px; }
#qtext::selection { background:var(--sel); }
#qtext:focus { outline:none; }
#qtext::placeholder { color:var(--faint); }
#qhl .k { color:var(--ink); font-weight:600; } /* RECALL, WHERE, LIMIT … */
#qhl .s { color:var(--hash); } /* "quoted strings" */
#qhl .n { color:var(--warn); } /* numbers */
#qhl .c { color:var(--faint); } /* -- comments */
.composer .cbar { display:flex; align-items:center; gap:9px; flex-wrap:wrap;
padding:11px 14px; border-top:1px solid var(--line);
background:var(--well); border-radius:0 0 12px 12px; }
.composer .kbd { font-size:11.5px; font-weight:500; opacity:.75; }
/* ── the on-ramp: what the page offers before you have run anything ── */
.onramp { display:flex; flex-direction:column; gap:12px; }
.qsec { display:flex; align-items:center; gap:11px; padding-top:10px; }
.qsec i { flex:1; height:1px; background:var(--line); }
/* Two across so the four questions read as a block, not a ragged row — and
sized to hold two even when the developer rail takes 330px off the width. */
.starters { display:grid; grid-template-columns:repeat(auto-fit, minmax(340px, 1fr)); gap:12px; }
.starter { text-align:left; background:var(--surface); border:1px solid var(--line);
border-radius:11px; padding:15px 16px; display:flex; flex-direction:column; gap:10px;
cursor:pointer; transition:border-color .12s, box-shadow .12s; }
.starter:hover { border-color:var(--accent-line); box-shadow:var(--card-shadow); }
.starter b { font-size:15px; font-weight:600; color:var(--ink); line-height:21px; }
/* the CAL each question writes, shown so the answer teaches the language */
.starter code { font-family:var(--mono); font-size:11.5px; color:var(--muted); background:var(--bg);
border:0; border-radius:6px; padding:8px 10px; display:block;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* ── analytics: the grain-type census, namespace breakdown, growth trend
and recall-leg status that used to live as a small "WHAT'S IN THIS
MEMORY" teaser on this very page — moved to its own tab so it could
cover all 12 grain types instead of 4, without crowding the on-ramp.
`.crow` is the shared row shape for both the grain-type census
and the namespace breakdown — a mono key, an optional gloss, a bar
proportional to that list's own max (not a global constant), and a
right-aligned count. */
.stiles { display:grid; grid-template-columns:repeat(4, minmax(200px, 1fr)); gap:14px; }
.stile { padding:18px 20px; display:flex; flex-direction:column; gap:6px; }
.stile .lbl { font-size:11.5px; font-weight:600; color:var(--dim); letter-spacing:.09em; }
.stile .n { font-size:28px; font-weight:700; color:var(--ink); letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
.stile .sub { font-size:12.5px; color:var(--faint); }
.census { display:flex; flex-direction:column; margin-top:6px; }
.crow { display:flex; align-items:center; gap:14px; padding:9px 0; border:0; background:none;
width:100%; text-align:left; font-family:inherit; cursor:pointer; }
.crow .k { width:150px; flex-shrink:0; font-family:var(--mono); font-size:12.5px; color:var(--ink); }
.crow .k.wide { width:200px; }
.crow .g { width:230px; flex-shrink:0; font-size:12.5px; color:var(--faint);
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.crow .track { flex:1; height:8px; background:var(--raise); border-radius:4px; }
.crow .bar { height:100%; background:var(--accent); border-radius:4px; }
.crow .bar.zero { background:var(--line); }
.crow .n { width:40px; flex-shrink:0; text-align:right; font-size:14px; font-weight:600;
color:var(--ink); font-variant-numeric:tabular-nums; }
.crow .n.zero { color:var(--faint); font-weight:500; }
.gchart { display:flex; align-items:flex-end; gap:6px; height:100px; margin-top:22px; }
.gbar { flex:1; background:var(--accent-line); border-radius:4px 4px 0 0; }
.gbar.today { background:var(--accent); }
.legs { display:flex; gap:14px; }
.leg { flex:1; padding:16px 18px; display:flex; flex-direction:column; gap:8px; }
.leg .hd { display:flex; align-items:center; gap:8px; }
.leg .dot { width:7px; height:7px; border-radius:4px; flex-shrink:0; }
.leg b { font-size:14px; font-weight:600; color:var(--ink); }
.leg .d { font-size:12.5px; color:var(--faint); line-height:18px; }
/* minmax(0,·) or a long description sets the column's min-content width and
the two halves stop being halves. */
.recents { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:12px 26px; }
.rrow { display:flex; flex-direction:column; gap:3px; padding:9px 0; text-align:left;
background:none; border:0; border-bottom:1px solid var(--hair); cursor:pointer; width:100%; }
.rrow:hover .rq, .rrow:hover .rn { color:var(--accent); }
/* A recent run is code and reads as code; a saved query has a human name. */
.rrow .rq { font-family:var(--mono); font-size:11.5px; color:var(--text);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.rrow .rn { font-size:14px; color:var(--ink); font-weight:500;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.rrow .rm { font-size:12.5px; color:var(--faint);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.rempty { padding:9px 0; font-size:13px; color:var(--faint); }
/* Restate the query in plain words, so a non-technical reader can tell at a
glance whether the CAL asked what they meant. */
.qsum { display:flex; flex-direction:column; gap:3px; padding-bottom:13px; }
.qsum .h { font-size:19px; font-weight:600; color:var(--ink); letter-spacing:-.01em; }
.qsum .h em { font-style:normal; font-weight:400; font-size:15px; color:var(--muted); }
#qbar { display:flex; align-items:center; gap:12px; flex-wrap:wrap;
padding:12px 0; border-bottom:1px solid var(--line); }
#qmore { padding:16px 0; }
#qerr { background:var(--danger-soft); border:1px solid var(--danger-line); border-radius:10px;
padding:12px 14px; font-size:13px; }
#qerr .code { font-family:var(--mono); font-size:11px; color:var(--danger);
border:1px solid var(--danger-line); border-radius:5px; padding:1px 6px; margin-right:8px; }
#qerr pre { margin-top:8px; font-family:var(--mono); font-size:12.5px; color:var(--muted); white-space:pre-wrap; }
#qerr .hint { margin-top:7px; color:var(--warn); }
code { font-family:var(--mono); font-size:.92em; background:var(--raise); border:1px solid var(--line);
border-radius:5px; padding:1px 5px; }
.menuwrap { position:relative; display:inline-block; }
/* A drawn chevron rather than the ▾ glyph, which renders thin and pale at
this size and disappears against a ghost button. */
.btn.hasmenu::after { content:''; width:6px; height:6px; flex-shrink:0; margin-left:2px;
border-right:1.7px solid currentColor; border-bottom:1.7px solid currentColor;
transform:translateY(-2px) rotate(45deg); }
/* JSON is the developer's view of the answer — colour it the way the rest of
this console colours values, so the meanings carry across. */
.jsonv { line-height:19px; }
.jsonv .jk { color:var(--ink); font-weight:600; }
.jsonv .js { color:var(--hash); }
.jsonv .jn { color:var(--warn); }
.jsonv .jb { color:var(--ok); font-weight:500; }
.jsonv .jz { color:var(--faint); }
.menu { position:absolute; z-index:30; top:calc(100% + 6px); right:0; background:var(--surface);
border:1px solid var(--line2); border-radius:11px; min-width:300px; max-width:460px;
max-height:340px; overflow-y:auto; box-shadow:var(--shadow); padding:5px; }
.menu .mi { padding:8px 11px; border-radius:8px; cursor:pointer; font-size:13px; color:var(--text); }
.menu .mi:hover { background:var(--raise); }
.menu .mi small { display:block; font-family:var(--mono); font-size:11px; color:var(--dim);
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:2px; }
.menu .sep { padding:8px 11px 4px; font-size:11px; font-weight:600; letter-spacing:.09em; color:var(--dim); }
/* order-preserving masonry: cards keep result order, heights vary with content */
#qcards { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));
grid-auto-rows:8px; gap:0 16px; }
/* `layoutMasonry()` reserves a gutter's worth of extra rows past each
card's own content height — but a grid item stretches to fill its full
row-span by default, so without `align-self:start` the card's own box
grows to swallow that reserved space and the "gap" never becomes a gap. */
.qcard { align-self:start; background:var(--surface); border:1px solid var(--line); border-radius:12px;
padding:14px 16px; display:flex; flex-direction:column; gap:8px;
box-shadow:var(--card-shadow); overflow:hidden; }
.qcard .hd { display:flex; align-items:center; gap:8px; }
.qcard .kind { font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; }
.qcard .say { font-size:15px; line-height:22px; color:var(--ink); overflow-wrap:anywhere; }
.qcard .kv { display:flex; gap:8px; font-size:12.5px; padding:3px 0; border-top:1px solid var(--hair); }
.qcard .kv b { flex-shrink:0; min-width:84px; color:var(--dim); font-weight:500; }
.qcard .kv span { color:var(--text); overflow-wrap:anywhere; min-width:0; }
#qtable { width:100%; border-collapse:collapse; font-size:13.5px; }
#qtable th { text-align:left; font-size:11px; font-weight:600; letter-spacing:.06em; color:var(--dim);
text-transform:uppercase; padding:8px 12px 8px 0; border-bottom:1px solid var(--line2); white-space:nowrap; }
#qtable td { padding:9px 12px 9px 0; border-bottom:1px solid var(--hair); vertical-align:top;
max-width:340px; overflow-wrap:anywhere; }
.seg.sm button { height:26px; padding:0 11px; font-size:12.5px; }
/* The graph is a canvas, not prose — let it use the width, within reason. */
#graphView { width:100%; max-width:var(--wide); margin-left:auto; margin-right:auto; }
/* ── rail ── */
/* Nav items carry their own 11px left padding (room for the hover/active
pill around the icon), and the rail footer already insets by that same
11px to match — the brand row was the one place that didn't, so its
icon sat 11px left of every nav icon below it. */
.brand { display:flex; align-items:center; gap:9px; padding-left:11px; border:0; background:none;
font-family:inherit; text-align:left; cursor:pointer; width:100%; border-radius:9px;
padding-top:4px; padding-bottom:4px; margin:-4px 0; transition:background .12s; }
.brand:hover { background:var(--raise); }
.brand b { font-size:17px; font-weight:700; color:var(--ink); letter-spacing:-.02em; }
.nav { display:flex; flex-direction:column; gap:2px; }
.nav-item { display:flex; align-items:center; gap:11px; padding:9px 11px; border-radius:9px;
cursor:pointer; color:var(--text); font-size:15px; font-weight:500; border:0; background:none;
width:100%; text-align:left; font-family:inherit; transition:background .12s; }
.nav-item:hover { background:var(--raise); }
.nav-item.on { background:var(--accent-soft); color:var(--accent); font-weight:600; }
.nav-item svg { flex-shrink:0; stroke:currentColor; }
.nav-item .grow { flex:1; }
.pill { display:inline-flex; align-items:center; justify-content:center; min-width:20px; height:20px;
padding:0 6px; border-radius:10px; background:var(--warn); color:#fff; font-size:11px; font-weight:600; }
.pill.soft { background:var(--raise); color:var(--dim); }
.nav-item.on .pill.soft { background:var(--surface); color:var(--accent); }
.sec-label { font-size:11.5px; font-weight:600; color:var(--dim); letter-spacing:.09em; }
#entities { display:flex; flex-direction:column; gap:1px; overflow-y:auto; min-height:0; }
.ent { display:flex; align-items:center; gap:10px; padding:7px 11px; border-radius:8px; cursor:pointer;
border:0; background:none; width:100%; text-align:left; font-family:inherit; transition:background .12s; }
.ent:hover { background:var(--raise); }
.ent.on { background:var(--accent-soft); }
.ent .av { display:flex; align-items:center; justify-content:center; width:22px; height:22px; flex-shrink:0;
border-radius:11px; background:var(--raise); color:var(--muted); font-size:10.5px; font-weight:600; }
.ent.on .av { background:var(--accent); color:#fff; }
.ent .nm { flex:1; font-size:14.5px; font-weight:500; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ent .ct { width:22px; flex-shrink:0; text-align:right; font-size:13px; color:var(--faint); font-variant-numeric:tabular-nums; }
.rail-foot { margin-top:auto; display:flex; flex-direction:column; gap:14px; padding:0 11px 2px; }
.health { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted); cursor:pointer; }
.dot { width:7px; height:7px; border-radius:4px; background:var(--ghost); flex-shrink:0; }
.toggle-row { display:flex; align-items:center; gap:10px; padding-top:14px; border-top:1px solid var(--line); }
.toggle-row span { flex:1; font-size:13.5px; color:var(--dim); }
.sw { width:34px; height:20px; flex-shrink:0; border-radius:10px; background:var(--line2); padding:2px;
display:flex; align-items:center; cursor:pointer; border:0; transition:background .15s; }
.sw i { width:16px; height:16px; border-radius:8px; background:#fff; display:block; transition:transform .15s; }
.sw.on { background:var(--accent); }
.sw.on i { transform:translateX(14px); }
/* ── type ── */
h1 { font:700 31px/38px var(--sans); color:var(--ink); letter-spacing:-.025em; }
h2 { font:700 15px var(--sans); color:var(--ink); letter-spacing:-.01em; }
.sub { font-size:14.5px; line-height:22px; color:var(--muted); }
.meta { font-size:13px; color:var(--dim); }
.tiny { font-size:12.5px; color:var(--faint); }
.mono { font-family:var(--mono); font-size:11.5px; color:var(--hash); }
/* ── page header: one breadcrumb line — home icon, prior pages, then the
current page as the trail's own last segment (not a separate display
headline below it) — followed by a description line. Every page's
title block, one component. All trail text shares one font size; color
and weight carry the hierarchy instead: prior pages read legibly rather
than near-invisible, the current-page segment is the cleanest, darkest,
boldest text in the row, and the description recedes a step further
neutral than either. ── */
.pagehead { display:flex; flex-direction:column; gap:3px; }
.pagehead .trail { display:flex; flex-wrap:wrap; align-items:center; gap:6px; row-gap:2px; }
.pagehead .trail .home { display:flex; align-items:center; padding:2px; margin-left:-2px; border:0; background:none; color:var(--dim); border-radius:5px; transition:color .12s; }
.pagehead .trail .home:hover { color:var(--accent); }
/* The "›" sits 1px low against the icon/text either side of it — nudge
it down that 1px rather than the icon (which is already centred). */
.pagehead .trail .sep { font-size:13.5px; color:var(--faint); padding-bottom:1px; }
.pagehead .trail .link { border:0; background:none; padding:0; font:500 13.5px var(--sans); color:var(--dim); cursor:pointer; }
.pagehead .trail .link:hover { color:var(--accent); }
.pagehead .ctitle { font:600 13.5px/19px var(--sans); color:var(--ink); letter-spacing:-.01em; }
.pagehead .cdesc { font-size:13.5px; line-height:20px; color:var(--muted); margin-top:2px; }
/* Editable title/description (Workflows only): the same element throughout
hover and focus — never swapped for a different tag — so turning it into
an active field never shifts anything around it. The padding/negative-
margin pair keeps the text sitting in exactly the same spot whether or
not the hover/focus background is showing. The input never carries its
own width: it sits in a grid cell sized by an invisible twin (::after,
same text via data-value) so the field hugs its content and the edit/
lock icons sit right after the text instead of stranded at the row's
far edge. */
.pagehead .edit-row { display:inline-flex; align-items:center; gap:5px; max-width:100%; vertical-align:middle; }
/* The sizer span carries the same ctitle/cdesc class as the input inside
it, so it (and its ::after, which inherits font from it for free) picks
up the exact same font as the real title/description text below —
no font:inherit here, that would out-specificity .pagehead .ctitle/
.cdesc and silently fall back to an ambient size. ::after and the input
also share identical padding/border so the ghost's measured width
already includes the room the input's own padding will eat into. */
.pagehead .input-sizer { display:inline-grid; align-items:center; min-width:1em; max-width:100%; }
.pagehead .input-sizer::after, .pagehead .input-sizer input {
grid-area:1/1; box-sizing:border-box; padding:1px 5px; border:1px solid transparent; }
.pagehead .input-sizer::after { content:attr(data-value) ' '; visibility:hidden; white-space:pre; }
.pagehead input.ctitle, .pagehead input.cdesc {
display:block; width:100%; min-width:1em; height:auto; background:none;
border-radius:6px; margin:-1px -5px; cursor:pointer; }
.pagehead input.ctitle:hover:not(:focus):not(:disabled), .pagehead input.cdesc:hover:not(:focus):not(:disabled) {
background:var(--raise); border-color:var(--line); cursor:text; }
.pagehead input.ctitle:focus, .pagehead input.cdesc:focus {
background:var(--surface); border-color:var(--accent-line); box-shadow:0 0 0 3px var(--accent-soft); cursor:text; outline:none; }
.pagehead input.ctitle:disabled, .pagehead input.cdesc:disabled { color:var(--dim); cursor:default; }
.pagehead input.ctitle::placeholder, .pagehead input.cdesc::placeholder { color:var(--faint); }
.pagehead .edit-icon { display:flex; align-items:center; flex-shrink:0; color:var(--faint); opacity:0; transition:opacity .12s; cursor:pointer; }
.pagehead .edit-row:hover .edit-icon { opacity:1; }
.pagehead .lock-hint { display:flex; align-items:center; flex-shrink:0; color:var(--warn); cursor:help; margin-left:2px; }
/* ── controls ── */
button { font-family:inherit; cursor:pointer; }
.btn { display:inline-flex; align-items:center; justify-content:center; gap:7px; height:40px; padding:0 20px;
border-radius:10px; background:var(--accent); color:#fff; border:0; font-size:14.5px; font-weight:600;
transition:background .12s; white-space:nowrap; flex-shrink:0; }
.btn:hover { background:var(--accent-hover); }
.btn.sm { height:34px; padding:0 15px; font-size:13px; border-radius:9px; }
.btn.ghost { background:var(--surface); border:1px solid var(--line2); color:var(--text); font-weight:500; }
.btn.ghost:hover { background:var(--raise); }
.btn.danger { background:none; border:0; color:var(--danger); font-weight:600; padding:0; height:auto; }
.btn:disabled { opacity:.5; cursor:default; }
.chip { padding:6px 13px; border-radius:16px; border:1px solid var(--line); background:var(--surface);
font-size:13.5px; color:var(--text); cursor:pointer; transition:border-color .12s, color .12s; }
.chip:hover { border-color:var(--line2); }
.chip.on { background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600; }
.seg { display:inline-flex; padding:3px; border-radius:10px; background:var(--raise); }
.seg button { height:30px; padding:0 15px; border-radius:8px; border:0; background:none; color:var(--muted);
font-size:13.5px; font-weight:500; }
.seg button.on { background:var(--surface); color:var(--accent); font-weight:600; box-shadow:0 1px 2px rgba(11,13,18,.06); }
.tabs { display:flex; align-items:flex-end; gap:26px; border-bottom:1px solid var(--line); }
.tabs button { padding:0 0 11px; border:0; background:none; font-size:14.5px; font-weight:500; color:var(--muted); }
.tabs button.on { color:var(--accent); font-weight:600; box-shadow:inset 0 -2px 0 var(--accent); }
input[type=text], textarea { font-family:inherit; background:var(--surface); border:1px solid var(--line2);
border-radius:10px; color:var(--ink); padding:9px 12px; font-size:14px; width:100%; }
input[type=text]:focus, textarea:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
/* A native <select>, restyled to match every other control here instead of
the browser's own chrome — `appearance:none` plus a drawn chevron.
Chrome renders no pseudo-elements on <select>, so the chevron is a real
sibling span (`.sel-wrap`/`.sel-arrow`), the same shape `.ns-combo`'s
own arrow already uses, not a ::after. */
select { appearance:none; -webkit-appearance:none; font-family:inherit; background:var(--surface);
border:1px solid var(--line2); border-radius:10px; color:var(--ink); padding:9px 30px 9px 12px;
font-size:14px; width:100%; cursor:pointer; }
select:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
select:disabled { cursor:default; opacity:.6; }
.sel-wrap { position:relative; display:block; }
.sel-arrow { position:absolute; right:11px; top:0; height:100%; display:flex; align-items:center;
pointer-events:none; color:var(--faint); }
/* ── ask ── */
.askbar { display:flex; align-items:center; gap:13px; height:56px; padding:0 14px 0 18px; background:var(--surface);
border:1px solid var(--line2); border-radius:13px; transition:border-color .12s, box-shadow .12s; }
.askbar:focus-within { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
.askbar input { border:0; background:none; padding:0; font-size:16px; flex:1; height:100%; }
.askbar input:focus { outline:none; box-shadow:none; }
.askbar input::placeholder { color:var(--faint); }
.examples { display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:0 2px; }
.examples .tiny { flex-shrink:0; }
/* ── cards & banners ── */
/* On a white page a card needs its own lift, not fill contrast. */
.card { background:var(--surface); border:1px solid var(--line); border-radius:14px; box-shadow:var(--card-shadow); }
.banner { display:flex; align-items:center; gap:12px; padding:13px 16px; border-radius:11px;
background:var(--warn-soft); border:1px solid var(--warn-line); }
.banner .grow { flex:1; font-size:14.5px; color:var(--text); }
.banner b { color:var(--ink); font-weight:600; }
.banner .go { flex-shrink:0; font-size:14px; font-weight:600; color:var(--accent); cursor:pointer; }
.empty { padding:34px 0; text-align:center; color:var(--dim); font-size:14px; }
.planv { display:flex; flex-direction:column; padding-top:6px; }
.prow { display:flex; gap:14px; padding:10px 0; border-bottom:1px solid var(--hair); font-size:13.5px; }
.prow b { width:104px; flex-shrink:0; color:var(--dim); font-weight:500; }
.prow span { color:var(--text); font-family:var(--mono); font-size:12.5px; }
.pnote { margin-top:14px; padding:12px 14px; background:var(--warn-soft);
border:1px solid var(--warn-line); border-radius:9px; font-size:13px;
line-height:20px; color:var(--text); }
/* A query that matched nothing is a normal outcome, not an error — say what
was asked and offer the next thing to try, rather than a dead end. */
.nores { display:flex; flex-direction:column; align-items:flex-start; gap:9px;
padding:30px 26px; background:var(--surface); border:1px solid var(--line);
border-radius:12px; }
.nores .t { font-size:17px; font-weight:600; color:var(--ink); }
.nores .d { font-size:14px; line-height:21px; color:var(--muted); max-width:62ch; }
.nores .acts { display:flex; gap:9px; flex-wrap:wrap; padding-top:4px; }
/* Colour carries meaning here, never decoration: teal is a content address
everywhere in this console, burnt orange is "needs attention". */
.v-hash { color:var(--hash); font-family:var(--mono); font-size:11.5px; }
.v-time { color:var(--dim); }
.v-yes { color:var(--ok); font-weight:500; }
.v-no { color:var(--warn); font-weight:500; }
.v-num { color:var(--ink); font-weight:500; font-variant-numeric:tabular-nums; }
/* ── memory list ── */
.grp-head { display:flex; align-items:baseline; gap:10px; padding:22px 0 10px; }
.grp-head:first-child { padding-top:6px; }
.mrow { display:flex; align-items:flex-start; gap:18px; padding:14px 8px 14px 0; border-top:1px solid var(--hair);
cursor:pointer; border-radius:8px; transition:background .1s; }
.mrow:hover { background:var(--raise); }
.mrow.sel { background:var(--accent-soft); }
.mrow .body { flex:1; display:flex; flex-direction:column; gap:4px; min-width:0; }
.mrow .say { font-size:16.5px; line-height:25px; color:var(--ink); }
.mrow .prov { font-size:13px; line-height:20px; color:var(--dim); }
.mrow .when { width:96px; flex-shrink:0; text-align:right; font-size:13px; color:var(--dim); padding-top:4px; }
.mrow.gone .say { color:var(--ghost); text-decoration:line-through; }
.mrow.sup .say { color:var(--muted); }
/* ── answer ── */
#answer { display:flex; flex-direction:column; padding:28px 30px 0; }
.ans-label { font-size:11.5px; font-weight:600; color:var(--accent); letter-spacing:.09em; }
.ans-text { font-size:20px; line-height:31px; color:var(--ink); letter-spacing:-.01em; }
.srow { display:flex; align-items:center; gap:14px; padding:11px 0; border-bottom:1px solid var(--hair); cursor:pointer; }
.srow:last-child { border-bottom:0; }
.srow .t { flex:1; font-size:15px; color:var(--ink); }
.srow .w { width:96px; flex-shrink:0; text-align:right; font-size:13px; color:var(--dim); }
.disclose { display:flex; align-items:center; gap:10px; padding:15px 0 18px; border-top:1px solid var(--hair);
cursor:pointer; font-size:14px; color:var(--text); }
.disclose .grow { flex:1; }
.disclose .caret { transition:transform .15s; }
.disclose.open .caret { transform:rotate(90deg); }
pre.code { font-family:var(--mono); font-size:13px; line-height:21px; color:var(--ink); background:var(--well);
border:1px solid var(--hair); border-radius:10px; padding:14px 16px; overflow-x:auto; white-space:pre-wrap;
word-break:break-word; }
/* ── graph ── */
#gwrap { background:var(--well); border:1px solid var(--line); border-radius:16px; overflow:hidden; }
/* The query workbench draws its own result graph (#qstage/#qcanvas/#qzoom);
it shares this chrome so the two graphs read — and handle — the same. */
#gstage, #qstage { position:relative; }
#gcanvas { display:block; width:100%; height:560px; cursor:grab; }
#qcanvas { display:block; width:100%; height:460px; cursor:grab;
background:var(--well); border:1px solid var(--line); border-radius:14px; }
#gcanvas.grabbing, #qcanvas.grabbing { cursor:grabbing; }
.float { position:absolute; background:var(--surface); border:1px solid var(--line); border-radius:12px; box-shadow:var(--shadow); }
#gtools { top:16px; left:16px; display:flex; align-items:center; gap:10px; background:none; border:0; box-shadow:none; }
#glegend { bottom:16px; left:16px; display:flex; align-items:center; gap:14px; height:34px; padding:0 14px; border-radius:17px; }
#glegend span { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--text); white-space:nowrap; }
#gzoom { bottom:16px; right:16px; display:flex; align-items:center; height:34px; padding:0 4px; border-radius:17px; }
#qzoom { bottom:14px; right:14px; display:flex; align-items:center; height:34px; padding:0 4px; border-radius:17px; }
#gzoom button, #qzoom button { width:30px; height:28px; border:0; background:none; color:var(--text); font-size:16px; border-radius:14px; }
#gzoom button.lbl, #qzoom button.lbl { width:auto; padding:0 10px; font-size:12px; font-weight:600; color:var(--accent); }
#ginspect { top:16px; right:16px; width:288px; padding:18px; display:flex; flex-direction:column; gap:14px; }
#gscrub { display:flex; flex-direction:column; gap:11px; padding:16px 20px 18px; background:var(--surface); border-top:1px solid var(--line); }
#gscrub input[type=range] { width:100%; accent-color:var(--accent); }
.scrub-ticks { display:flex; justify-content:space-between; font-size:11.5px; color:var(--faint); }
/* ── workflows ── */
/* The plan list reuses .starter (query page's card) — a workflow card is
the same shape: a bold label plus a code-styled gloss line. */
#wfList { display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:12px; align-items:start; }
#wfwrap { background:var(--well); border:1px solid var(--line); border-radius:16px; overflow:hidden; }
#wfstage { position:relative; }
/* The canvas is the reason this page exists — give it the room, not a
token box. Scales with the viewport rather than a fixed guess, with a
floor for short windows. */
#wfcanvas { display:block; width:100%; height:calc(100vh - 260px); min-height:480px; cursor:default; }
#wfcanvas.connecting { cursor:crosshair; }
#wftools { top:16px; left:16px; display:flex; align-items:center; gap:9px; background:none; border:0; box-shadow:none; }
#wflegend { bottom:16px; left:16px; display:flex; align-items:center; flex-wrap:wrap; gap:8px 14px;
min-height:34px; max-width:min(560px, 62%); padding:7px 14px; border-radius:15px; }
#wflegend span { display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--text); white-space:nowrap; }
#wflegend .legsep { color:var(--ghost); }
#wfzoom { bottom:16px; right:16px; display:flex; align-items:center; height:34px; padding:0 4px; border-radius:17px; }
#wfzoom button { width:30px; height:28px; border:0; background:none; color:var(--text); font-size:16px; border-radius:14px; }
#wfzoom button.lbl { width:auto; padding:0 10px; font-size:12px; font-weight:600; color:var(--accent); }
#wfinspect { top:16px; right:16px; width:302px; padding:18px; display:flex; flex-direction:column; gap:13px; max-height:488px; overflow-y:auto; }
.wf-badge { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:10px; font-size:11.5px; font-weight:600; width:fit-content; }
.wf-field { display:flex; flex-direction:column; gap:5px; }
.wf-field .tiny { color:var(--dim); }
/* A read-only inspector row: the trigger panel is all of these, because a
trigger cannot be authored through CAL and so must not offer inputs that
look editable. */
.wf-ro { display:flex; flex-direction:column; gap:3px; }
.wf-ro .k { font-size:12.5px; color:var(--dim); }
.wf-ro .v { font-size:13.5px; color:var(--ink); overflow-wrap:anywhere; }
.wf-ro .v.mono { font-family:var(--mono); font-size:12px; color:var(--text); }
/* ── the run bar: which run the canvas is tinted by ──
It sits in the page header rather than floating over the canvas, where it
would collide with the tools (top-left) and the inspector (top-right) at
narrow widths. */
.runbar { display:flex; align-items:center; gap:10px; height:34px; padding:0 6px 0 12px;
border:1px solid var(--line2); border-radius:9px; background:var(--surface); flex-shrink:0; }
.runbar .dot { width:8px; height:8px; border-radius:5px; flex-shrink:0; }
.runbar .lbl { font-size:12.5px; color:var(--text); white-space:nowrap; }
.runbar .lbl b { color:var(--ink); font-weight:600; }
.runbar .tally { font-size:12px; color:var(--dim); white-space:nowrap; }
.runbar select { height:26px; width:auto; max-width:230px; border:0; background:none; color:var(--ink);
font-size:12.5px; font-weight:600; font-family:inherit; padding:0 24px 0 8px;
line-height:26px; text-overflow:ellipsis; border-radius:7px; }
.runbar select:hover { background:var(--raise); }
.runbar .sel-wrap { display:flex; align-items:center; width:auto; height:26px; }
.runbar .sel-arrow { right:7px; }
.runbar .go { border:0; background:none; color:var(--accent); font-size:12.5px; font-weight:600;
padding:0 8px; height:26px; border-radius:7px; white-space:nowrap; }
.runbar .go:hover { background:var(--accent-soft); }
/* ── runs ── */
/* Grouped, not one flat grid: "waiting on you" is a different kind of thing
from "finished last Tuesday", and a single list buries the first in the
second. */
.rungroup { display:flex; flex-direction:column; gap:12px; }
.rungroup + .rungroup { margin-top:30px; }
.rungroup > .hd { display:flex; align-items:baseline; gap:10px; }
.rungroup > .hd h3 { font-size:13px; font-weight:600; color:var(--dim); letter-spacing:.07em;
text-transform:uppercase; }
.rungroup > .hd .tiny { color:var(--faint); }
.rungrid { display:grid; grid-template-columns:repeat(auto-fit, minmax(400px, 1fr)); gap:12px; align-items:start; }
.runcard { padding:0; overflow:hidden; display:flex; flex-direction:column; }
.runcard .top { display:flex; align-items:center; gap:11px; padding:15px 18px 13px; }
.runcard .top .plan { flex:1; min-width:0; font-size:15px; font-weight:600; color:var(--ink);
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.runcard .top .plan .rid { display:block; font-size:12px; font-weight:400; color:var(--faint);
font-family:var(--mono); }
.runcard .meta { display:flex; flex-wrap:wrap; gap:5px 14px; padding:0 18px 14px; font-size:12.5px; color:var(--dim); }
.runcard .steps { display:flex; gap:3px; padding:0 18px 15px; }
.runcard .steps i { height:4px; flex:1; border-radius:2px; background:var(--line); }
.runcard .foot { display:flex; align-items:center; gap:14px; padding:12px 18px;
border-top:1px solid var(--hair); background:var(--well); }
.runcard .foot .grow { flex:1; }
.ask { margin:0 18px 16px; padding:13px 15px; border-radius:10px;
background:var(--warn-soft); border:1px solid var(--warn-line); }
.ask b { color:var(--ink); font-weight:600; font-size:13.5px; }
.ask .who { font-size:12.5px; color:var(--muted); margin-top:3px; }
.ask .acts { display:flex; gap:14px; align-items:center; margin-top:11px; }
.status-tag { display:inline-flex; align-items:center; gap:6px; padding:3px 10px; border-radius:10px;
font-size:11.5px; font-weight:600; flex-shrink:0; }
/* ── tools ── */
#toolBody { display:flex; flex-direction:column; gap:14px; }
.toolgrid { display:grid; grid-template-columns:repeat(auto-fit, minmax(340px, 1fr)); gap:12px; align-items:start; }
.toolcard { text-align:left; background:var(--surface); border:1px solid var(--line); border-radius:14px;
box-shadow:var(--card-shadow); padding:16px 18px; display:flex; flex-direction:column; gap:8px;
cursor:pointer; font-family:inherit; }
.toolcard:hover { border-color:var(--accent-line); }
.toolcard .hd { display:flex; align-items:center; gap:10px; }
.toolcard .hd b { flex:1; min-width:0; font-size:15px; font-weight:600; color:var(--ink);
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.toolcard .desc { font-size:13.5px; color:var(--text); line-height:19px; }
.toolcard .use { font-size:12.5px; color:var(--faint); }
/* One execution = one line. A run of 40 calls has to stay scannable, so the
detail opens in place rather than each row carrying its payload. */
.exec { display:flex; align-items:center; gap:12px; width:100%; padding:11px 16px; border:0; background:none;
border-top:1px solid var(--hair); text-align:left; font-family:inherit; cursor:pointer; }
.exec:first-child { border-top:0; }
.exec:hover { background:var(--raise); }
.exec .bar { width:3px; align-self:stretch; border-radius:2px; flex-shrink:0; }
.exec .nm { flex:1; min-width:0; font-size:13.5px; color:var(--ink); font-weight:500;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.exec .nm span { color:var(--faint); font-weight:400; }
.exec .when { font-size:12px; color:var(--faint); white-space:nowrap; flex-shrink:0; }
.execwrap { display:flex; flex-direction:column; }
.execdet { padding:2px 16px 16px 31px; display:flex; flex-direction:column; gap:12px; background:var(--well);
border-top:1px solid var(--hair); }
/* Reuses the query workbench's own JSON block (pre.code.jsonv) — same
highlighting, just tighter, because these sit inside a list row. */
.execdet pre.code { font-size:11.5px; line-height:17.5px; background:var(--surface);
border:1px solid var(--line); border-radius:9px; padding:11px 13px;
max-height:300px; overflow:auto; white-space:pre-wrap; overflow-wrap:anywhere; }
.execdet .lab { font-size:11.5px; font-weight:600; color:var(--dim); letter-spacing:.07em; }
.schema { display:flex; flex-direction:column; }
.schema .r { display:flex; align-items:baseline; gap:12px; padding:7px 0; border-top:1px solid var(--hair); }
.schema .r:first-child { border-top:0; }
.schema .r .n { width:170px; flex-shrink:0; font-family:var(--mono); font-size:12.5px; color:var(--ink); }
.schema .r .t { font-size:12.5px; color:var(--hash); font-family:var(--mono); }
.schema .r .req { font-size:11px; font-weight:600; color:var(--warn); }
.schema .r .d { flex:1; min-width:0; font-size:12.5px; color:var(--dim); }
/* ── namespace combobox: a real dropdown (known namespaces, click to
pick) that is also just a text field — type a prefix pattern like
"org.*" and it's accepted the same as any exact name. Shared by every
page with a namespace picker, rendered as one pill ("Namespace: value
⌄") rather than each page inventing its own label placement — that
drift was exactly what made the header row's height/position jump
between pages that have this control and pages that don't. */
.ns-combo { position:relative; display:inline-flex; align-items:stretch; height:34px;
border-radius:9px; background:var(--surface); border:1px solid var(--line2);
cursor:text; flex-shrink:0; }
/* A shaded, static tag for "Namespace" — its own background, rounded to
match the pill's own left corners (no `overflow:hidden` on `.ns-combo`
for that — the dropdown menu is an absolutely-positioned child of it and
would get clipped too), and a hairline border to its right — so it
reads as the fixed label, leaving the plain-background value beside it
looking like the part you can edit. */
.ns-combo .lbl { display:flex; align-items:center; padding:0 11px; background:var(--raise);
color:var(--dim); font-size:13px; flex-shrink:0; border-right:1px solid var(--line2);
border-radius:8px 0 0 8px; }
.ns-combo .value { display:flex; align-items:center; gap:8px; padding:0 12px; }
/* Auto-width value: an invisible ::after twin (same text, via data-value)
sizes the grid cell the input shares with it, so the pill hugs "caller"
as tightly as it hugs "org.billing" — the same trick the editable page
title uses (see .pagehead .input-sizer above). `input[type=text]`'s own
rule outranks a single `.ns-combo-input` class on specificity, so every
override below is scoped through `.ns-combo` to actually win. */
.ns-combo .sizer { display:inline-grid; align-items:center; min-width:1em; }
.ns-combo .sizer::after, .ns-combo .ns-combo-input { grid-area:1/1; box-sizing:border-box; }
.ns-combo .sizer::after { content:attr(data-value) ' '; visibility:hidden; white-space:pre; font:inherit; }
.ns-combo .ns-combo-input { border:0; background:none; padding:0; height:auto; width:100%; min-width:1em;
font:600 13px var(--sans); color:var(--ink); cursor:text; }
.ns-combo .ns-combo-input:focus { outline:none; box-shadow:none; }
.ns-combo-arrow { display:flex; align-items:center; color:var(--faint); flex-shrink:0; pointer-events:none; }
.ns-combo-menu { position:absolute; z-index:20; top:calc(100% + 4px); right:0; left:auto; min-width:240px;
background:var(--surface); border:1px solid var(--line2); border-radius:10px;
box-shadow:var(--shadow); max-height:230px; overflow-y:auto; padding:4px; }
.ns-combo-opt { padding:7px 9px; border-radius:7px; font-size:13px; color:var(--text); cursor:pointer; }
.ns-combo-opt:hover, .ns-combo-opt.on { background:var(--raise); }
.ns-combo-opt.hint { color:var(--accent); font-family:var(--mono); font-size:12px; }
.ns-combo-empty { padding:7px 9px; font-size:13px; color:var(--faint); }
/* ── suggestions ── */
/* Page-level stacks own the rhythm between blocks; `.stack` is an inner
group whose own rows carry their separators, so it takes no gap. */
#sugList, #conBody { display:flex; flex-direction:column; gap:16px; }
.stack { display:flex; flex-direction:column; }
.sug { display:flex; flex-direction:column; gap:16px; padding:24px 28px 22px; }
.tag { display:inline-flex; align-items:center; gap:7px; padding:4px 10px 4px 8px; border-radius:14px; font-size:12px; font-weight:600; }
.evi { display:flex; flex-direction:column; background:var(--well); border:1px solid var(--hair); border-radius:11px; padding:2px 16px; }
.evi-row { display:flex; align-items:center; gap:12px; padding:12px 0; border-bottom:1px solid var(--hair); }
.evi-row:last-child { border-bottom:0; }
.evi-row .t { flex:1; font-size:15px; color:var(--ink); }
.evi-row .w { width:140px; flex-shrink:0; text-align:right; font-size:13px; color:var(--dim); }
.acts { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.acts .note { flex:1; text-align:right; font-size:13.5px; color:var(--dim); }
/* The confirm step replaces the action row rather than sitting under it —
three live buttons at once is a guessing game. */
.reason { display:flex; flex-direction:column; gap:9px; padding:14px 16px;
background:var(--well); border:1px solid var(--line); border-radius:11px; }
.reason .line { display:flex; align-items:center; gap:10px; }
/* ── activity ── */
.ev { display:flex; align-items:flex-start; gap:16px; padding:14px 0; border-top:1px solid var(--hair); }
.ev .at { width:52px; flex-shrink:0; font-size:13px; color:var(--dim); padding-top:3px; }
.ev .ic { width:18px; flex-shrink:0; display:flex; justify-content:center; padding-top:5px; }
.ev .body { flex:1; display:flex; flex-direction:column; gap:3px; min-width:0; }
.ev .say { font-size:15.5px; color:var(--ink); }
.ev-tags { display:flex; align-items:center; gap:8px; flex-shrink:0; padding-top:4px; }
.ev-tags .tag { padding:3px 9px; border-radius:8px; font-size:11px; }
/* ── connect ── */
.apps { display:flex; gap:16px; }
.app { flex:1; display:flex; flex-direction:column; gap:8px; background:var(--well); border:1px solid var(--line);
border-radius:12px; padding:14px 16px; }
.app .hd { display:flex; align-items:center; gap:9px; }
.app .hd b { flex:1; font-size:14.5px; font-weight:600; color:var(--ink); }
.conns { display:flex; }
.conns div { flex:1; display:flex; justify-content:center; }
.conns i { width:2px; height:22px; background:var(--line2); display:block; border-radius:1px; }
.hubbar { display:flex; align-items:center; gap:14px; background:var(--accent-soft); border:1.5px solid var(--accent);
border-radius:12px; padding:16px 18px; }
.kv { display:flex; align-items:baseline; gap:12px; padding:9px 0; border-top:1px solid var(--hair); }
.kv .k { width:190px; flex-shrink:0; font-size:13.5px; color:var(--dim); }
.kv .v { flex:1; font-size:14px; color:var(--ink); word-break:break-word; }
/* ── panel ── */
#panelWrap { position:fixed; inset:0; z-index:40; }
#panelShade { position:absolute; inset:0; background:rgba(11,13,18,.35); backdrop-filter:blur(2px); }
#panel { position:absolute; top:0; right:0; bottom:0; width:min(430px,96vw); background:var(--surface);
border-left:1px solid var(--line2); box-shadow:var(--shadow); padding:28px 30px 32px; overflow-y:auto;
display:flex; flex-direction:column; gap:24px; }
.pgroup { display:flex; flex-direction:column; gap:14px; padding-top:20px; border-top:1px solid var(--hair); }
.step { display:flex; gap:14px; }
.step .rail { display:flex; flex-direction:column; align-items:center; width:10px; flex-shrink:0; gap:4px; }
.step .bul { width:10px; height:10px; border-radius:5px; background:var(--ok); flex-shrink:0; }
.step .bul.past { background:var(--surface); border:1.5px solid var(--line2); }
.step .ln { width:1px; flex:1; background:var(--line); }
/* ── dev sidebar ── */
#devbar { width:330px; flex-shrink:0; background:var(--bg); border-left:1px solid var(--line);
padding:26px 22px 28px; display:flex; flex-direction:column; gap:24px; position:sticky; top:0;
height:100vh; overflow-y:auto; }
.oprow { display:flex; align-items:center; gap:10px; padding:7px 0; border-bottom:1px solid var(--hair);
cursor:pointer; font-family:var(--mono); font-size:11.5px; }
.oprow .t { width:30px; flex-shrink:0; color:var(--ghost); }
.oprow .o { width:70px; flex-shrink:0; }
/* What the operation wrote, in words — the address is on hover. Sans, because
this is prose now; teal mono only when we fall back to a bare hash. */
.oprow .h { flex:1; min-width:0; font-family:var(--sans); font-size:12.5px; color:var(--text);
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.oprow .h.addr { font-family:var(--mono); font-size:11.5px; color:var(--hash); }
.oprow .h.sys { color:var(--faint); } /* the engine talking to itself */
/* ── toast ── */
#toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--ink); color:var(--bg);
border-radius:10px; padding:10px 20px; font-size:14px; z-index:99; box-shadow:var(--shadow); display:none; }
@media (max-width:1180px) { #devbar { display:none !important; } }
@media (max-width:900px) { #rail { display:none; } .page { padding:28px 22px 44px; } }
/* Last word: `hidden` must beat every id-level display rule above. */
[hidden] { display:none !important; }
</style>
<script>
(function () {
var q = new URLSearchParams(location.search);
var t = q.get('theme') || localStorage.getItem('areev.theme');
if (t === 'light' || t === 'dark') document.documentElement.dataset.theme = t;
var d = q.get('dev');
if (d === '1' || (d !== '0' && localStorage.getItem('areev.dev') === '1'))
document.documentElement.classList.add('dev');
})();
</script>
<div id="shell">
<nav id="rail">
<button class="brand" id="brandBtn" type="button" title="Memory">
<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><rect x="7.5" y="2.5" width="14" height="14" rx="4.5" fill="none" stroke="var(--accent-line)" stroke-width="2"/><rect x="2.5" y="7.5" width="14" height="14" rx="4.5" fill="var(--accent)"/></svg>
<b>Areev</b>
</button>
<div class="nav" id="nav">
<button class="nav-item on" data-page="memory">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M5 4.5A1.5 1.5 0 016.5 3H17a1.5 1.5 0 011.5 1.5V21l-6.5-3.4L5.5 21V4.5z" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="grow">Memory</span>
</button>
<button class="nav-item" data-page="query">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M4.5 6.5h15M4.5 12h9M4.5 17.5h12" stroke-width="1.7" stroke-linecap="round"/><path d="M16.5 15.5l2.2 2.2" stroke-width="1.7" stroke-linecap="round"/><circle cx="15" cy="14" r="2.6" stroke-width="1.7"/></svg>
<span class="grow">Query</span>
</button>
<button class="nav-item" data-page="activity">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M3 12.5h4.2l2.6-7 4.2 13 2.4-6h4.6" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="grow">Activity</span>
</button>
<!-- Plan before run before catalog: the order you meet them in. A
trigger has no nav item of its own — it is only meaningful next to
the plan it starts, so it lives on the workflow canvas. -->
<button class="nav-item" data-page="workflows">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="5.5" cy="6" r="2.3" stroke-width="1.7"/><circle cx="18.5" cy="6" r="2.3" stroke-width="1.7"/><circle cx="12" cy="18" r="2.3" stroke-width="1.7"/><path d="M7.6 7.2L11 16M16.4 7.2L13 16M7.8 6h8.4" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="grow">Workflows</span>
<span class="pill soft" id="wfTrgPill" hidden>0</span>
</button>
<button class="nav-item" data-page="runs">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M5 4.5v15l13-7.5-13-7.5z" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="grow">Runs</span>
<span class="pill" id="runPill" hidden>0</span>
</button>
<button class="nav-item" data-page="tools">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M9 2.8v5.4M15 2.8v5.4" stroke-width="1.7" stroke-linecap="round"/><path d="M5.8 8.2h12.4v3.2a6.2 6.2 0 01-12.4 0V8.2z" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 17.6v3.6" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="grow">Tools</span>
</button>
<button class="nav-item" data-page="suggestions">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M12 3.5l1.9 4.9 4.9 1.9-4.9 1.9L12 17.1l-1.9-4.9L5.2 10.3l4.9-1.9L12 3.5z" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="grow">Suggestions</span>
<span class="pill" id="sugPill" hidden>0</span>
</button>
<button class="nav-item" data-page="analytics">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M5 19V13.5M11.5 19V8.5M18 19V5" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="grow">Analytics</span>
</button>
<button class="nav-item" data-page="settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke-width="1.7"/><path d="M19.4 13.5a1.7 1.7 0 000-3l-1-.2a6.7 6.7 0 00-.7-1.7l.6-.9a1.7 1.7 0 00-2.4-2.4l-.9.6a6.7 6.7 0 00-1.7-.7l-.2-1a1.7 1.7 0 00-3 0l-.2 1a6.7 6.7 0 00-1.7.7l-.9-.6a1.7 1.7 0 00-2.4 2.4l.6.9a6.7 6.7 0 00-.7 1.7l-1 .2a1.7 1.7 0 000 3l1 .2c.15.6.4 1.17.7 1.7l-.6.9a1.7 1.7 0 002.4 2.4l.9-.6c.53.3 1.1.55 1.7.7l.2 1a1.7 1.7 0 003 0l.2-1c.6-.15 1.17-.4 1.7-.7l.9.6a1.7 1.7 0 002.4-2.4l-.6-.9c.3-.53.55-1.1.7-1.7l1-.2z" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="grow">Settings</span>
</button>
</div>
<div class="rail-foot">
<div class="health" id="health" title="check every stored memory against its content address">
<span class="dot" id="hdot"></span><span id="htext">checking…</span>
</div>
<div class="toggle-row" id="whorow" style="display:none;border:0;padding-top:0">
<span id="whoami" title="the principal this session is bound to">…</span>
<button id="signbtn" style="background:none;border:0;color:var(--accent);cursor:pointer;font:inherit;padding:0">Sign in</button>
</div>
<span class="tiny" id="dblabel" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{DB}}</span>
</div>
</nav>
<main id="main">
<!-- ── MEMORY ── -->
<section class="page" id="page-memory">
<div style="display:flex;align-items:flex-end;gap:16px" class="wide">
<div id="memCrumb" style="flex:1;min-width:0"></div>
<div class="seg" id="mview">
<button class="on" data-v="list">List</button>
<button data-v="graph">Graph</button>
</div>
</div>
<div id="listView" style="display:flex;flex-direction:column;gap:24px">
<div class="wide" style="display:flex;flex-direction:column;gap:11px">
<div class="askbar">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" style="flex-shrink:0"><circle cx="11" cy="11" r="6.6" stroke="var(--accent)" stroke-width="1.8"/><path d="M16 16l4.2 4.2" stroke="var(--accent)" stroke-width="1.8" stroke-linecap="round"/></svg>
<input type="text" id="ask" placeholder="Search your memories — or ask a question and press Enter" autocomplete="off">
<button class="btn sm" id="askBtn">Ask</button>
</div>
<div class="examples" id="examples"></div>
</div>
<div class="banner wide" id="sugBanner" hidden>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" style="flex-shrink:0"><path d="M12 3.5l1.9 4.9 4.9 1.9-4.9 1.9L12 17.1l-1.9-4.9L5.2 10.3l4.9-1.9L12 3.5z" stroke="var(--warn)" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="grow" id="sugBannerText"></span>
<span class="go" id="sugBannerGo">Review →</span>
</div>
<div class="wide" id="filterBar" hidden></div>
<div class="card wide" id="answer" hidden></div>
<div class="wide" id="memList"><div class="empty">loading…</div></div>
</div>
<div id="graphView" hidden>
<div id="gwrap">
<div id="gstage">
<canvas id="gcanvas"></canvas>
<div class="float" id="gtools">
<div style="display:flex;align-items:center;gap:9px;height:36px;width:250px;padding:0 12px;background:var(--surface);border:1px solid var(--line);border-radius:10px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" style="flex-shrink:0"><circle cx="11" cy="11" r="6.6" stroke="var(--dim)" stroke-width="1.9"/><path d="M16 16l4.2 4.2" stroke="var(--dim)" stroke-width="1.9" stroke-linecap="round"/></svg>
<input type="text" id="gjump" placeholder="Jump to anyone…" style="border:0;background:none;padding:0;font-size:13.5px">
</div>
<div class="seg" id="gdepth" style="background:var(--surface);border:1px solid var(--line)">
<button class="on" data-d="1">Direct</button><button data-d="2">2 steps</button>
</div>
</div>
<div class="float" id="glegend"></div>
<div class="float" id="gzoom">
<button id="gout" title="zoom out">−</button><button id="gin" title="zoom in">+</button><button class="lbl" id="gfit">Fit</button>
</div>
<div class="float" id="ginspect" hidden></div>
</div>
<div id="gscrub">
<div style="display:flex;align-items:center;gap:9px">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" style="flex-shrink:0"><circle cx="12" cy="12" r="8.5" stroke="var(--muted)" stroke-width="1.8"/><path d="M12 7.5V12l3 2" stroke="var(--muted)" stroke-width="1.8" stroke-linecap="round"/></svg>
<b style="font-size:13.5px;color:var(--ink)">Rewind</b>
<span class="meta" style="flex:1">drag to see what your agent knew at any point — nothing is ever deleted from the history</span>
<span class="chip on" id="gnow" style="height:26px;padding:0 12px;display:inline-flex;align-items:center">now</span>
</div>
<input type="range" id="grange" min="0" max="100" value="100">
<div class="scrub-ticks" id="gticks"></div>
</div>
</div>
</div>
</section>
<!-- ── ANALYTICS ── -->
<section class="page" id="page-analytics" hidden>
<div class="wide" style="display:flex;align-items:flex-start;gap:16px">
<div id="anCrumb" style="flex:1;min-width:0"></div>
<span id="anNsHost"></span>
</div>
<div class="wide stiles" id="anTiles"></div>
<div class="wide qsec"><span class="sec-label">WHAT'S IN THIS MEMORY</span><i></i>
<span class="tiny">All 12 grain types this memory can hold — click one to run it in Query</span></div>
<div class="wide census" id="anCensus"></div>
<div class="wide qsec"><span class="sec-label">NAMESPACES</span><i></i>
<span class="tiny">Every namespace this session can read — click one to scope the page to it</span></div>
<div class="wide census" id="anNsBreakdown"></div>
<div class="wide qsec"><span class="sec-label">GROWTH</span><i></i>
<span class="tiny" id="anGrowthCaption"></span></div>
<div class="wide" id="anGrowth"></div>
<div class="wide qsec"><span class="sec-label">HOW IT CAN BE QUERIED</span><i></i>
<span class="tiny">The recall legs active on this file</span></div>
<div class="wide legs" id="anLegs"></div>
</section>
<!-- ── ACTIVITY ── -->
<section class="page" id="page-activity" hidden>
<div class="wide" style="display:flex;align-items:flex-start;gap:16px">
<div id="actCrumb" style="flex:1;min-width:0"></div>
<span id="actNsHost"></span>
</div>
<div class="examples wide" id="actFilters"></div>
<div class="wide" id="actList"><div class="empty">loading…</div></div>
</section>
<!-- ── SUGGESTIONS ── -->
<section class="page" id="page-suggestions" hidden>
<div id="sugCrumb" class="wide"></div>
<div class="wide" style="display:flex;align-items:center;gap:16px">
<div class="tabs" id="sugTabs" style="flex:1">
<button class="on" data-s="pending">Needs review</button>
<button data-s="applied">Applied</button>
<button data-s="rejected">Dismissed</button>
</div>
<button class="btn sm" id="runLoop">Run analysis</button>
</div>
<div class="wide" id="sugList"><div class="empty">loading…</div></div>
</section>
<!-- ── RUNS: the governed runtime viewer + HITL approval queue ── -->
<section class="page" id="page-runs" hidden>
<div id="runCrumb" class="wide"></div>
<div class="wide" id="runList"><div class="empty">loading…</div></div>
</section>
<!-- ── WORKFLOWS: plans as an editable graph ──
One action slot, top right, for whatever this view's primary act is:
"New workflow" on the list, "Save as new plan" in the editor. -->
<!-- Tools: what a step can call, and every call that was made. Both are
Tool grains split by `kind` — one catalog answering two questions
("what can run" / "what did run"), so two tabs rather than two pages.
The namespace picker is deliberately the SAME state the Workflows
page uses (WF_NS): a plan and the tools it binds are one operations
surface, and two independent pickers on the same rows drift. -->
<section class="page" id="page-tools" hidden>
<div id="toolCrumb" class="wide"></div>
<div class="wide" style="display:flex;align-items:center;gap:16px" id="toolTabsRow">
<div class="tabs" id="toolTabs" style="flex:1">
<button class="on" data-t="catalog">Catalog</button>
<button data-t="executions">Executions</button>
</div>
<span id="toolNsHost"></span>
</div>
<div class="wide" id="toolBody"></div>
</section>
<section class="page" id="page-workflows" hidden>
<div class="wide" style="display:flex;align-items:flex-start;gap:16px">
<div id="wfCrumb" style="flex:1;min-width:0"></div>
<span id="wfRunHost"></span>
<span id="wfNsHost"></span>
<button class="btn sm" id="wfNewBtn">New workflow</button>
<button class="btn sm" id="wfSave" hidden>Save as new plan</button>
</div>
<div class="wide" id="wfList"><div class="empty">loading…</div></div>
<div class="wide" id="wfOrphans" hidden></div>
<div id="wfEditorWrap" hidden style="display:flex;flex-direction:column;gap:12px;flex:1;min-height:0;">
<div class="wide" id="wfwrap" style="flex:1;min-height:0;">
<div id="wfstage">
<canvas id="wfcanvas"></canvas>
<div class="float" id="wftools">
<button class="btn sm" id="wfAddNode">+ Node</button>
<button class="btn ghost sm" id="wfResetLayout">Reset layout</button>
<span class="tiny" id="wfHint">Drag a step's dot to connect it</span>
</div>
<div class="float" id="wflegend"></div>
<div class="float" id="wfzoom">
<button id="wfout" title="zoom out">−</button><button id="wfin" title="zoom in">+</button><button class="lbl" id="wffit">Fit</button>
</div>
<div class="float" id="wfinspect" hidden></div>
</div>
</div>
<div class="col" id="wfReasonBox"></div>
</div>
</section>
<!-- ── SETTINGS (formerly "Connect" — Agent/Sync tabs plus General,
which now also carries what used to sit in the rail-foot:
developer mode and dark mode) ── -->
<section class="page" id="page-settings" hidden>
<div id="setCrumb" class="wide"></div>
<div class="tabs wide" id="conTabs">
<button class="on" data-c="agent">Your agent</button>
<button data-c="sync">Sync across apps</button>
<button data-c="general">General</button>
</div>
<div class="wide" id="conBody"></div>
</section>
<!-- ── QUERY ── -->
<section class="page" id="page-query" hidden>
<div class="wide" style="display:flex;align-items:center;gap:16px">
<div id="queryCrumb" style="flex:1;min-width:0"></div>
<button class="btn ghost sm" id="qreset" hidden>New query</button>
</div>
<span class="wide sub" id="qintro">Ask the memory a precise question. Every view in this console
runs the same language underneath — start from an example below and edit it.</span>
<div class="wide composer">
<div class="qedit">
<div id="qhl" aria-hidden="true"></div>
<textarea id="qtext" spellcheck="false" rows="3"
placeholder='RECALL facts WHERE subject = "john" LIMIT 20'></textarea>
</div>
<div class="cbar">
<button class="btn sm" id="qrun">Run <span class="kbd">⌘⏎</span></button>
<button class="btn ghost sm" id="qexplain">Explain</button>
<button class="btn ghost sm" id="qsave">Save</button>
<div class="menuwrap"><button class="btn ghost sm hasmenu" id="fmtBtn">Format</button>
<div class="menu" id="fmtMenu" hidden></div></div>
<div style="flex:1"></div>
<span class="tiny" id="qnotice"></span>
</div>
</div>
<div class="wide" id="qerr" hidden></div>
<div class="wide onramp" id="qonramp">
<div class="qsec"><span class="sec-label">START FROM A QUESTION</span><i></i>
<span class="tiny">Each one writes real CAL into the editor — edit it from there</span></div>
<div class="starters" id="qstarters"></div>
<div class="qsec"><span class="sec-label">SAVED & RECENT</span><i></i>
<div class="menuwrap"><button class="btn ghost sm hasmenu" id="savedBtn">All saved</button>
<div class="menu" id="savedMenu" hidden></div></div>
<div class="menuwrap"><button class="btn ghost sm hasmenu" id="histBtn">Full history</button>
<div class="menu" id="histMenu" hidden></div></div></div>
<div class="recents" id="qrecents"></div>
</div>
<div class="wide" id="qresult" hidden>
<div id="qsummary"></div>
<div id="qbar"></div>
<div id="qbody"></div>
<div id="qmore" hidden></div>
</div>
</section>
</main>
<aside id="devbar" class="dev-only dev-flex">
<div style="display:flex;flex-direction:column;gap:11px">
<span class="sec-label">STORE</span>
<div id="devStats"></div>
</div>
<div style="display:flex;flex-direction:column;gap:0">
<span class="sec-label" style="padding-bottom:12px">OP LOG</span>
<div id="devLog"></div>
</div>
<div style="display:flex;flex-direction:column;gap:9px;margin-top:auto">
<span class="sec-label">CAL</span>
<span class="tiny">Every view here runs CAL underneath. The full workbench —
history, saved queries, templates and every output format — lives on the Query page.</span>
<button class="btn ghost sm" id="devQuery">Open Query →</button>
</div>
</aside>
</div>
<div id="panelWrap" hidden><div id="panelShade"></div><aside id="panel"></aside></div>
<div id="toast"></div>
<script>
'use strict';
const $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<')
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
const el = (tag, cls, txt) => { const n = document.createElement(tag); if (cls) n.className = cls;
if (txt != null) n.textContent = txt; return n; };
/* Wrap a native <select> with a drawn chevron (see the `select`/`.sel-wrap`
CSS) so it reads as one of this console's own controls rather than the
browser's default box — real native behavior underneath, just restyled. */
function styledSelect(select) {
const wrap = el('div', 'sel-wrap');
wrap.appendChild(select);
const arrow = el('span', 'sel-arrow');
arrow.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="M5 9l7 7 7-7" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
wrap.appendChild(arrow);
return wrap;
}
/* ══ page header ══════════════════════════════════════════════════════
One component for every page: a breadcrumb trail (home icon › section ›
optional current item) plus a description line beneath it. Every page
calls this instead of hand-rolling its own <h1>/.sub — home always jumps
to Memory; crumbs is an array of {label, onClick?} with the trailing,
onClick-less entry rendered as the bold "you are here" segment. */
const HOME_ICON = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M4 11.5L12 4l8 7.5M6.5 10v9h11v-9" '
+ 'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const EDIT_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M16.5 3.5l4 4L8 20l-5 1 1-5L16.5 3.5z" '
+ 'stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>';
const LOCK_ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="1.8"/>'
+ '<path d="M8 11V7a4 4 0 018 0v4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
/* An inline-editable title/description field: always the same <input>, in
display state or focused/active state — never swapped for another
element — so becoming editable never shifts the layout around it. A
pencil icon fades in on row hover as the "this is editable" cue; clicking
the text itself works just as well. cfg: {value, placeholder, disabled,
onCommit(newValue)}. lockHint, if given, adds a small icon carrying it as
a native tooltip (used for "why can't I edit this"). */
function editableField(cls, cfg, lockHint) {
const row = el('div', 'edit-row');
// The sizer's data-value drives an invisible ::after twin (same text, same
// font) that the input shares a grid cell with — the cell (and so the
// input) is exactly as wide as the text/placeholder, never wider, so the
// edit/lock icons that follow sit right against the text instead of
// stranded at the far edge of a full-width field.
const sizer = el('span', 'input-sizer ' + cls);
sizer.dataset.value = cfg.value || cfg.placeholder || '';
const input = document.createElement('input');
input.type = 'text'; input.className = cls; input.autocomplete = 'off';
input.value = cfg.value || '';
input.placeholder = cfg.placeholder || '';
input.disabled = !!cfg.disabled;
input.addEventListener('input', () => { sizer.dataset.value = input.value || cfg.placeholder || ''; });
input.addEventListener('blur', () => {
const v = input.value.trim();
if (v !== (cfg.value || '')) cfg.onCommit(v);
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
else if (e.key === 'Escape') { input.value = cfg.value || ''; sizer.dataset.value = cfg.value || cfg.placeholder || ''; input.blur(); }
});
sizer.appendChild(input);
row.appendChild(sizer);
if (!cfg.disabled) {
const icon = el('span', 'edit-icon'); icon.innerHTML = EDIT_ICON;
icon.onclick = () => input.focus();
row.appendChild(icon);
}
if (lockHint) {
const lock = el('span', 'lock-hint'); lock.innerHTML = LOCK_ICON; lock.title = lockHint;
row.appendChild(lock);
}
return row;
}
/* opts (optional): { editTitle, editDesc: <editableField cfg>, lockHint }.
The current-page segment (either the last crumb's plain label, or the
editable title field when editTitle is set) renders IN the trail row,
right after its own leading "›" — one line, home icon through to "you
are here" — never a separate headline below it. descHtml (or editDesc)
is the one line beneath. */
function renderCrumbHeader(hostId, crumbs, descHtml, opts) {
const host = $(hostId); if (!host) return;
opts = opts || {};
host.textContent = '';
const wrap = el('div', 'pagehead');
const trail = el('div', 'trail');
const home = el('button', 'home'); home.innerHTML = HOME_ICON; home.title = 'Memory'; home.onclick = () => go('memory');
trail.appendChild(home);
const priorCrumbs = opts.editTitle ? crumbs : crumbs.slice(0, -1);
for (const c of priorCrumbs) {
trail.appendChild(el('span', 'sep', '›'));
if (c.onClick) { const b = el('button', 'link', c.label); b.onclick = c.onClick; trail.appendChild(b); }
else trail.appendChild(el('span', 'link', c.label));
}
trail.appendChild(el('span', 'sep', '›'));
if (opts.editTitle) trail.appendChild(editableField('ctitle', opts.editTitle, opts.lockHint));
else trail.appendChild(el('span', 'ctitle', crumbs[crumbs.length - 1].label));
wrap.appendChild(trail);
if (opts.editDesc) wrap.appendChild(editableField('cdesc', opts.editDesc, null));
else if (descHtml) { const d = el('div', 'cdesc'); d.innerHTML = descHtml; wrap.appendChild(d); }
host.appendChild(wrap);
}
/* ══ api ══════════════════════════════════════════════════════════════ */
let TOKEN = sessionStorage.getItem('areev.token') || '';
async function api(url, opts) {
opts = Object.assign({}, opts);
if (TOKEN) opts.headers = Object.assign({}, opts.headers, { Authorization: 'Bearer ' + TOKEN });
let r;
try { r = await fetch(url, opts); } catch (e) { return { ok: false, error: 'network error' }; }
if (r.status === 401) {
const t = window.prompt('This console is protected. Paste its token:');
if (t) { TOKEN = t; sessionStorage.setItem('areev.token', t); return api(url, opts); }
return { ok: false, error: 'not authorised' };
}
try { return await r.json(); } catch (e) { return { ok: false, error: 'bad response' }; }
}
const cal = q => api('/api/cal', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: q }) });
/* Identity: in multi-principal mode (areev ui --auth) the rail shows who
this session is bound to; Sign in stores a bearer token, Sign out
drops back to the read-only anonymous session. */
async function refreshWhoami() {
const w = await api('/api/whoami');
if (!w || !w.ok || w.mode !== 'credentials') return;
const row = $('whorow'); row.style.display = '';
$('whoami').textContent = w.owner ? 'Owner session' : w.principal;
$('signbtn').textContent = TOKEN ? 'Sign out' : 'Sign in';
}
document.addEventListener('DOMContentLoaded', () => {
const btn = $('signbtn');
if (btn) btn.addEventListener('click', () => {
if (TOKEN) { TOKEN = ''; sessionStorage.removeItem('areev.token'); }
else {
const t = window.prompt('Paste your access token:');
if (!t) return;
TOKEN = t; sessionStorage.setItem('areev.token', t);
}
location.reload();
});
refreshWhoami();
});
/* ══ helpers ══════════════════════════════════════════════════════════ */
function toast(msg) {
const t = $('toast'); t.textContent = msg; t.style.display = 'block';
clearTimeout(toast._h); toast._h = setTimeout(() => { t.style.display = 'none'; }, 2000);
}
function copyText(s, label) {
if (!navigator.clipboard) { toast('copy unavailable'); return; }
navigator.clipboard.writeText(s).then(() => toast((label || 'Copied') + ' ✓'), () => toast('Copy failed'));
}
function download(name, text, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], { type: mime || 'application/json' }));
a.download = name; a.click(); URL.revokeObjectURL(a.href);
}
const hlcMs = h => Math.floor((h || 0) / 65536);
const shortHash = h => h ? h.slice(0, 12) + '…' : '';
const cap = s => s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
/* Capitalise names that read as words ("john" → "John"), but leave
identifiers alone — "Payment_gateway" and "Contract-9107" look wrong. */
const titleize = s => /^[a-z]+( [a-z]+)*$/.test(String(s || '')) ? cap(s) : String(s || '');
const lowerFirst = s => s ? s.charAt(0).toLowerCase() + s.slice(1) : '';
function relTime(ms) {
if (!ms) return '';
const d = Date.now() - ms;
if (d < 45000) return 'just now';
const m = Math.floor(d / 60000); if (m < 60) return m + (m === 1 ? ' minute ago' : ' minutes ago');
const h = Math.floor(m / 60); if (h < 24) return h + (h === 1 ? ' hour ago' : ' hours ago');
const dd = Math.floor(h / 24); if (dd < 30) return dd + (dd === 1 ? ' day ago' : ' days ago');
const mo = Math.floor(dd / 30); return mo + (mo === 1 ? ' month ago' : ' months ago');
}
const shortWhen = ms => {
if (!ms) return '';
const d = Date.now() - ms;
if (d < 45000) return 'now';
const m = Math.floor(d / 60000); if (m < 60) return m + 'm';
const h = Math.floor(m / 60); if (h < 24) return h + 'h';
const dd = Math.floor(h / 24); if (dd < 30) return dd + 'd';
return Math.floor(dd / 30) + 'mo';
};
const dayKey = ms => new Date(ms).toDateString();
function dayLabel(ms) {
const k = dayKey(ms), today = new Date().toDateString();
const y = new Date(Date.now() - 86400000).toDateString();
if (k === today) return 'TODAY';
if (k === y) return 'YESTERDAY';
return new Date(ms).toLocaleDateString(undefined, { day: 'numeric', month: 'long' }).toUpperCase();
}
const clock = ms => new Date(ms).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
const isoTitle = ms => ms ? new Date(ms).toISOString() : '';
/* created_at rides as epoch millis (a number) on stored grains, but an ISO
string on anything hand-written — accept both, never NaN. */
function createdMs(f) {
const c = f && f.created_at;
if (typeof c === 'number') return c;
if (typeof c === 'string') { const p = Date.parse(c); return isNaN(p) ? 0 : p; }
return 0;
}
/* Some writers (areev remember, memory-tool imports) tuck the human text
inside `context` rather than a top-level field. Dig for it — a memory
that renders as an empty row is worse than no row at all. */
function ctxText(f) {
const c = f && f.context;
if (!c || typeof c !== 'object') return '';
for (const k of ['content', 'text', 'message', 'summary', 'description']) {
if (typeof c[k] === 'string' && c[k].trim()) return c[k];
}
return '';
}
function preview(f) {
if (!f) return '';
const v = f.object != null ? f.object : f.content != null ? f.content
: f.description != null ? f.description : f.tool_content != null ? f.tool_content
: f.summary != null ? f.summary : f.title != null ? f.title
: ctxText(f) ? ctxText(f)
: f.data != null ? JSON.stringify(f.data) : '';
const s = typeof v === 'string' ? v : JSON.stringify(v);
return s.length > 140 ? s.slice(0, 140) + '…' : s;
}
const cssVar = n => getComputedStyle(document.documentElement).getPropertyValue(n).trim();
/* ══ language: a triple, said out loud ════════════════════════════════ */
const REL_PHRASE = {
prefers: 'Prefers {o}', likes: 'Likes {o}', dislikes: 'Dislikes {o}',
works_at: 'Works at {o}', employed_by: 'Works at {o}', member_of: 'Is a member of {o}',
lives_in: 'Lives in {o}', based_in: 'Is based in {o}', headquartered_in: 'Headquartered in {o}',
owns_project: 'Owns the {o} project', maintains: 'Maintains {o}', works_on: 'Works on {o}',
collaborates_with: 'Collaborates with {o}', knows: 'Knows {o}', reports_to: 'Reports to {o}',
asked_about: 'Asked about {o}', email: 'Email is {o}', phone: 'Phone number is {o}',
role: 'Role is {o}', title: 'Title is {o}', speaks: 'Speaks {o}', located_in: 'Is located in {o}',
};
/* No sentence is allowed to be a wall of text. A fact's object can be a
serialized record (the loop stores recommendations as Facts), and an
unbroken 1.5 KB of JSON has no wrap opportunities — it just blows the
column out sideways. */
const MAX_SAY = 180;
const clampText = s => s.length > MAX_SAY ? s.slice(0, MAX_SAY).trimEnd() + '…' : s;
function predicate(g) {
const f = g.fields || {};
if (g.type === 'fact') {
const r = f.relation || '', o = f.object;
const os = clampText(typeof o === 'string' ? o : JSON.stringify(o == null ? '' : o));
const tpl = REL_PHRASE[r];
if (tpl) return tpl.replace('{o}', os);
if (!r) return os;
return cap(r.replace(/_/g, ' ')) + ' ' + os;
}
const fallback = 'A ' + (g.type || 'memory') + ' with no readable text';
if (g.type === 'tool') {
const name = f.tool_name || 'A tool';
const body = f.tool_content || ctxText(f) || '';
return name + (f.is_error ? ' failed' : ' ran') + (body ? ' — ' + clampText(body) : '');
}
/* A run checkpoint is the runtime's own bookkeeping — its body is the
scheduler's serialized state, which has no sentence in it. Say which run
and which step it belongs to instead of calling it unreadable. */
if (g.type === 'state' && f.checkpoint && f.run_id != null)
return 'Checkpoint of run ' + f.run_id + (f.superstep != null ? ' \u00b7 step ' + f.superstep : '');
if (g.type === 'observation') return f.content || f.object || ctxText(f) || fallback;
if (g.type === 'event') return f.content || f.description || preview(f) || fallback;
if (g.type === 'goal') return f.description || f.content || preview(f) || fallback;
return preview(f) || fallback;
}
function sentence(g) {
const f = g.fields || {}, p = predicate(g);
if (!f.subject) return p;
const s = String(f.subject);
/* Observations and events are free text that usually names the subject
already — prefixing it again gives "john john asked about…". */
if (p.toLowerCase().startsWith(s.toLowerCase())) return cap(p);
return titleize(s) + ' ' + lowerFirst(p);
}
/* Where did this come from, in words a human can check. Returned in parts so
callers that already show a timestamp can drop the "Learned …" one. */
function provenanceParts(g) {
const f = g.fields || {}, bits = [];
const prevGrain = f.derived_from && BYHASH[f.derived_from];
const was = g._prevValue
|| (prevGrain ? (typeof (prevGrain.fields || {}).object === 'string'
? prevGrain.fields.object : preview(prevGrain.fields)) : null);
if (was) bits.push('Changed from ' + was);
const when = createdMs(f) || hlcMs(g.hlc);
if (!bits.length) bits.push('Learned ' + relTime(when));
if (g.type === 'observation') bits.push('noticed by your agent, not confirmed');
if (f.namespace && f.namespace !== 'shared') bits.push('in ' + f.namespace);
if (typeof f.confidence === 'number' && f.confidence < 0.75) bits.push('low confidence');
return bits;
}
const provenance = g => provenanceParts(g).join(' · ');
/* ══ state ════════════════════════════════════════════════════════════ */
let BROWSE = [], BYHASH = {}, STATS = {}, CONFIG = null, LOG = [], RECS = {}, TELEMETRY = null;
let PAGE = 'memory', MVIEW = 'list', SUGTAB = 'pending', CONTAB = 'agent', ACTFILTER = 'all';
let TOOLTAB = 'catalog', TOOL_SEL = null;
let ENTITY = null, ANSWER = null, SEL = null, ACTIVE_ENTS = [];
const isGone = g => !!(g.forgotten || g.missing || g.op === 'forget');
const isCurrent = g => !isGone(g) && !g.superseded_by && !g._stale;
/* Areev Loop keeps its own bookkeeping in the file (namespace "areev-loop",
subject "__loop_state__"). It is a real grain, but it is not something
the agent remembers *about anyone* — keep it out of the human views.
It stays reachable from the op-log and the CAL console. */
const INTERNAL_NS = new Set(['areev-loop']);
function isInternal(g) {
const f = g.fields || {};
if (INTERNAL_NS.has(f.namespace)) return true;
const s = f.subject;
return typeof s === 'string' && /^__.*__$/.test(s);
}
const currentGrains = () => BROWSE.filter(g => isCurrent(g) && !isInternal(g));
/* The store keeps one head per (namespace, subject, relation) — a plain
re-ADD of the same pair replaces the old value without logging a
SUPERSEDE, so /api/browse cannot tell us which row is live. Resolve it
the same way the store does: within a key, the newest op wins. Without
this the list shows a replaced value as if it were current. */
function headHashes(grains) {
const best = new Map();
for (const g of grains) {
if (isGone(g) || (g.type && g.type !== 'fact')) continue;
const f = g.fields || {};
if (!f.subject || !f.relation) continue;
const k = (f.namespace || '') + '