prototype: address owner review round 1 (#122)

- logo: restore the mirrored right half — the inline mark was missing the
  <use> mirror, rendering a lopsided single-page glyph
- verdict line: one uniform 22px line; counts inherit the display face
- font sizes up across the board (table 10->11px, titles 16/17->17/18px,
  stat figures 17->19px, sections 10->11px)
- overview Sites table: state column widens and never wraps
- topbar: Library + Log out grouped right, matching the original chrome
- Lanes: sidecar line moved under the Poll lanes explanation
- Resume renders inside the same pausebar slot as Pause
- lane problems (refusing / no browser / not checking) render danger red;
  a deliberate pause stays patina
- series notes: chips capped at two with a +N tail instead of stacking
- pending 'requested N ago' now renders once, in the action cell only
This commit is contained in:
2026-08-18 10:20:24 +07:00
parent f32108fba9
commit f988e68256
@@ -163,6 +163,12 @@ body[data-variant="B"] .sheet, body[data-variant="C"] .sheet {
padding: 22px 20px 14px; padding: 22px 20px 14px;
} }
.topbar form { margin: 0; } .topbar form { margin: 0; }
.topbar-actions {
display: flex;
align-items: center;
gap: 18px;
margin-left: auto;
}
.ghost { .ghost {
position: relative; position: relative;
@@ -210,7 +216,7 @@ main.page { padding: 0 20px 40px; }
.sec { .sec {
margin: 0; margin: 0;
padding: 18px 0 8px; padding: 18px 0 8px;
font: 500 10px/1 var(--font-mono); font: 500 11px/1 var(--font-mono);
letter-spacing: .2em; letter-spacing: .2em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -230,12 +236,8 @@ main.page { padding: 0 20px 40px; }
color: var(--paper); color: var(--paper);
} }
.verdict .attn { color: var(--patina); } .verdict .attn { color: var(--patina); }
.verdict .counts { .verdict .counts { font: inherit; color: var(--mute); }
font: 500 11px/1.5 var(--font-mono); .verdict .counts b { color: var(--paper-dim); font-weight: 400; }
letter-spacing: .08em;
color: var(--mute);
}
.verdict .counts b { color: var(--paper-dim); font-weight: 600; }
/* ---- stats block: label / figure rows, every figure a link but a zero ---- */ /* ---- stats block: label / figure rows, every figure a link but a zero ---- */
.stats { margin: 4px 0 0; } .stats { margin: 4px 0 0; }
@@ -248,13 +250,13 @@ main.page { padding: 0 20px 40px; }
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
} }
.stat .lbl { .stat .lbl {
font: 500 10px/1 var(--font-mono); font: 500 11px/1 var(--font-mono);
letter-spacing: .14em; letter-spacing: .14em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
} }
.stat .fig { .stat .fig {
font: 400 17px/1 var(--font-display); font: 400 19px/1 var(--font-display);
color: var(--paper); color: var(--paper);
} }
.stat a.fig:hover { color: var(--patina); } .stat a.fig:hover { color: var(--patina); }
@@ -262,15 +264,18 @@ main.page { padding: 0 20px 40px; }
.stat a.fig:hover { border-bottom-color: var(--patina); } .stat a.fig:hover { border-bottom-color: var(--patina); }
.stat .fig.zero { color: var(--mute); } .stat .fig.zero { color: var(--mute); }
/* ---- marks: patina is the admin accent; never ember, never danger ---- */ /* ---- marks: patina is the admin accent; never ember, never danger.
A lane in trouble overrides to danger (#122 review); paused stays
patina — deliberate, not a problem. ---- */
.mark, .mark-p { .mark, .mark-p {
font: 500 10px/1 var(--font-mono); font: 500 11px/1 var(--font-mono);
letter-spacing: .14em; letter-spacing: .14em;
text-transform: uppercase; text-transform: uppercase;
color: var(--patina); color: var(--patina);
white-space: nowrap; white-space: nowrap;
} }
.mark-faint { color: var(--mute-2); } .mark-faint { color: var(--mute-2); }
.mark.bad { color: var(--danger); }
/* ================= VARIANT A — narrow, prose rows (760px) ================= */ /* ================= VARIANT A — narrow, prose rows (760px) ================= */
/* No new tokens. Facts are a meta line under the title, like the reading /* No new tokens. Facts are a meta line under the title, like the reading
@@ -297,7 +302,7 @@ main.page { padding: 0 20px 40px; }
flex-wrap: wrap; flex-wrap: wrap;
gap: 3px 10px; gap: 3px 10px;
align-items: baseline; align-items: baseline;
font: 500 10px/1.5 var(--font-mono); font: 500 11px/1.5 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -309,7 +314,7 @@ main.page { padding: 0 20px 40px; }
.pst .pst-site { font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); } .pst .pst-site { font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); }
.pst .pst-facts { .pst .pst-facts {
margin-top: 4px; margin-top: 4px;
font: 500 10px/1.6 var(--font-mono); font: 500 11px/1.6 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -327,9 +332,9 @@ main.page { padding: 0 20px 40px; }
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
} }
.lrow .lane-site { font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); } .lrow .lane-site { font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); }
.lrow.attention .lane-site { color: var(--patina); } .lrow.attention .lane-site { color: var(--danger); }
.lrow .lane-fact { .lrow .lane-fact {
font: 500 10px/1 var(--font-mono); font: 500 11px/1 var(--font-mono);
letter-spacing: .14em; letter-spacing: .14em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -364,7 +369,7 @@ main.page { padding: 0 20px 40px; }
.tbl .thead span { .tbl .thead span {
padding: 10px 0 8px; padding: 10px 0 8px;
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
font: 500 10px/1 var(--font-mono); font: 500 11px/1 var(--font-mono);
letter-spacing: .14em; letter-spacing: .14em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -373,13 +378,13 @@ main.page { padding: 0 20px 40px; }
.tbl .trow > * { .tbl .trow > * {
padding: 11px 0; padding: 11px 0;
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
font: 500 10px/1.6 var(--font-mono); font: 500 11px/1.6 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
} }
.tbl .trow .c-title { .tbl .trow .c-title {
font: 400 16px/1.3 var(--font-display); font: 400 17px/1.3 var(--font-display);
letter-spacing: 0; letter-spacing: 0;
text-transform: none; text-transform: none;
color: var(--paper); color: var(--paper);
@@ -388,27 +393,30 @@ main.page { padding: 0 20px 40px; }
.tbl .trow.attention .c-title { color: var(--patina); } .tbl .trow.attention .c-title { color: var(--patina); }
.tbl .trow .c-site { letter-spacing: .04em; } .tbl .trow .c-site { letter-spacing: .04em; }
.tbl .trow .c-ch { color: var(--paper-dim); } .tbl .trow .c-ch { color: var(--paper-dim); }
.tbl .trow .c-note { color: var(--mute-2); } .tbl .trow .c-note { display: flex; flex-wrap: wrap; gap: 2px 8px; color: var(--mute-2); }
.tbl .trow .c-act { text-align: right; } .tbl .trow .c-act { text-align: right; }
.tbl .trow .c-act .ghost { font-size: 10px; } .tbl .trow .c-act .ghost { font-size: 11px; }
.tbl .trow > .confirm-row { grid-column: 1 / -1; } .tbl .trow > .confirm-row { grid-column: 1 / -1; }
.tbl .trow .c-act .ghost:hover { color: var(--patina); border-bottom-color: var(--patina); } .tbl .trow .c-act .ghost:hover { color: var(--patina); border-bottom-color: var(--patina); }
/* Landing per-Site table: site + library shape + five outcome sums over 12h. */ /* Landing per-Site table: site + library shape + five outcome sums over 12h. */
.tbl.sites { grid-template-columns: 120px 1fr repeat(8, 62px) 120px; } .tbl.sites { grid-template-columns: 120px 1fr repeat(8, 54px) minmax(150px, 1fr); }
.tbl.sites .trow > * { font: 500 10px/1.6 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--mute-2); } .tbl.sites .trow > * { font: 500 11px/1.6 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--mute-2); }
.tbl.sites .c-site { font: 400 17px/1.3 var(--font-display); letter-spacing: 0; text-transform: none; color: var(--paper-dim); } .tbl.sites .c-site { font: 400 18px/1.3 var(--font-display); letter-spacing: 0; text-transform: none; color: var(--paper-dim); }
.tbl.sites .c-state { color: var(--patina); } .tbl.sites .c-state { color: var(--patina); white-space: nowrap; }
.tbl.sites .c-state.bad { color: var(--danger); }
.tbl.sites .c-num { text-align: right; color: var(--paper-dim); } .tbl.sites .c-num { text-align: right; color: var(--paper-dim); }
/* Lanes page in B: one aligned row per Lane. */ /* Lanes page in B: one aligned row per Lane. */
.tbl.lanes { grid-template-columns: 130px 60px 70px 64px 96px 1fr 190px; } .tbl.lanes { grid-template-columns: 130px 60px 70px 64px 96px 1fr 170px; }
.tbl.lanes .trow > * { font: 500 10px/1.6 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--mute-2); } .tbl.lanes .trow > * { font: 500 11px/1.6 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--mute-2); }
.tbl.lanes .c-site { font: 400 17px/1.3 var(--font-display); letter-spacing: 0; text-transform: none; color: var(--paper-dim); } .tbl.lanes .c-site { font: 400 18px/1.3 var(--font-display); letter-spacing: 0; text-transform: none; color: var(--paper-dim); }
.tbl.lanes .trow.attention .c-site { color: var(--patina); } .tbl.lanes .trow.attention .c-site { color: var(--danger); }
.tbl.lanes .c-skip { color: var(--patina); } .tbl.lanes .c-skip { white-space: nowrap; }
.tbl.lanes .c-skip .bad { color: var(--danger); }
.tbl.lanes .c-skip .ok { color: var(--patina); }
.tbl.lanes .c-ctrl { text-align: right; } .tbl.lanes .c-ctrl { text-align: right; }
.tbl.lanes .c-ctrl .ghost { font-size: 10px; } .tbl.lanes .c-ctrl .ghost { font-size: 11px; }
/* ========== VARIANT C — wide, two-line blocks (no table, no card) ========= */ /* ========== VARIANT C — wide, two-line blocks (no table, no card) ========= */
/* Line one: title + action. Line two: every fact as one indented mono line. /* Line one: title + action. Line two: every fact as one indented mono line.
@@ -431,7 +439,7 @@ main.page { padding: 0 20px 40px; }
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 12px; gap: 4px 12px;
align-items: baseline; align-items: baseline;
font: 500 10px/1.6 var(--font-mono); font: 500 11px/1.6 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -444,7 +452,7 @@ main.page { padding: 0 20px 40px; }
.pblk .p1 .mark { font-size: 10px; } .pblk .p1 .mark { font-size: 10px; }
.pblk .p2 { .pblk .p2 {
margin-top: 4px; margin-top: 4px;
font: 500 10px/1.6 var(--font-mono); font: 500 11px/1.6 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -453,14 +461,14 @@ main.page { padding: 0 20px 40px; }
/* Lane rows in C: site + state line, facts + controls line. */ /* Lane rows in C: site + state line, facts + controls line. */
.lblk { padding: 10px 0; border-bottom: 1px solid var(--rule); } .lblk { padding: 10px 0; border-bottom: 1px solid var(--rule); }
.lblk .l1 { display: flex; align-items: baseline; gap: 14px; font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); } .lblk .l1 { display: flex; align-items: baseline; gap: 14px; font: 400 19px/1.2 var(--font-display); color: var(--paper-dim); }
.lblk.attention .l1 { color: var(--patina); } .lblk.attention .l1 { color: var(--danger); }
.lblk .l2 { .lblk .l2 {
margin-top: 4px; margin-top: 4px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 14px; gap: 4px 14px;
align-items: center; align-items: center;
font: 500 10px/1.6 var(--font-mono); font: 500 11px/1.6 var(--font-mono);
letter-spacing: .1em; letter-spacing: .1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--mute-2); color: var(--mute-2);
@@ -743,7 +751,7 @@ const SERIES = [
sighting: false, pending: false, orphan: true }, sighting: false, pending: false, orphan: true },
{ key: "demonic:return-of-the-sss-class-rankers", site: "demonic", { key: "demonic:return-of-the-sss-class-rankers", site: "demonic",
title: "Return of the SSS-Class Ranker", kind: "manga", title: "Return of the SSS-Class Ranker", kind: "manga",
ch: null, chLabel: "—", checked: null, readers: 1, cover: false, url: false, ch: null, chLabel: "—", checked: 13 * 60, readers: 1, cover: false, url: false,
sighting: false, pending: false }, sighting: false, pending: false },
{ key: "novelfull:reverend-insanity", site: "novelfull", { key: "novelfull:reverend-insanity", site: "novelfull",
title: "Reverend Insanity", kind: "novel", title: "Reverend Insanity", kind: "novel",
@@ -866,10 +874,18 @@ function rowMarks(s) {
if (!s.cover) m.push("no cover"); if (!s.cover) m.push("no cover");
if (s.sighting) m.push("reader sighting"); if (s.sighting) m.push("reader sighting");
if (s.readers === 0) m.push("orphan"); if (s.readers === 0) m.push("orphan");
if (s.pending) m.push(`requested ${age(s.pendingAgo)}`);
return m; return m;
} }
/* notes render as chips, capped at two with a +N tail — a wall of stacked
marks at list density reads as noise, not signal */
function noteChips(s) {
const m = rowMarks(s);
const chips = m.slice(0, 2).map(x => `<span class="mark">${x}</span>`).join("");
const more = m.length - 2;
return chips + (more > 0 ? `<span class="mark mark-faint">+${more}</span>` : "");
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* State: variant + page from the URL, filter/site/kind from search. */ /* State: variant + page from the URL, filter/site/kind from search. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -890,7 +906,7 @@ const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;"
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Shared chrome */ /* Shared chrome */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const mark = `<svg class="mark" viewBox="0 0 200 172" aria-hidden="true"><g fill="var(--ink)" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"><path fill="none" d="M28 36H4v114h192V36h-24"></path><path fill="none" d="M28 23H17v127h166V23h-11"></path><g><path d="M28 7 88 55v97L28 138z"></path><g fill="currentColor" stroke="none"><path d="M37 25 55 39v41L37 66z"></path><path d="M60 42 79 57v42L60 84z"></path><path d="M37 75 79 108v13L37 88z"></path><path d="M37 98 79 129v11L37 131z"></path></g></g><g stroke="var(--ember)"><path d="M100 4l9 5v11l-9 5-9-5V9z"></path><path d="M94 24h12v24H94z"></path><path d="M70 47h60v14H70z"></path><path d="M91 61h18v87l-9 20-9-20z"></path></g></g></svg>`; const mark = `<svg class="mark" viewBox="0 0 200 172" aria-hidden="true"><g fill="var(--ink)" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"><path fill="none" d="M28 36H4v114h192V36h-24"></path><path fill="none" d="M28 23H17v127h166V23h-11"></path><g id="mb-half"><path d="M28 7 88 55v97L28 138z"></path><g fill="currentColor" stroke="none"><path d="M37 25 55 39v41L37 66z"></path><path d="M60 42 79 57v42L60 84z"></path><path d="M37 75 79 108v13L37 88z"></path><path d="M37 98 79 129v11L37 131z"></path></g></g><use href="#mb-half" transform="matrix(-1 0 0 1 200 0)"></use><g stroke="var(--ember)"><path d="M100 4l9 5v11l-9 5-9-5V9z"></path><path d="M94 24h12v24H94z"></path><path d="M70 47h60v14H70z"></path><path d="M91 61h18v87l-9 20-9-20z"></path></g></g></svg>`;
function chrome(active) { function chrome(active) {
const nav = [ const nav = [
@@ -899,8 +915,10 @@ function chrome(active) {
`<a href="#${id}" class="${active === id ? "active" : ""}" ${active === id ? 'aria-current="page"' : ""}>${label}</a>`).join(""); `<a href="#${id}" class="${active === id ? "active" : ""}" ${active === id ? 'aria-current="page"' : ""}>${label}</a>`).join("");
return `<header class="topbar"> return `<header class="topbar">
<h1 class="brand">${mark}<span>Bookmark<em>Manager</em></span></h1> <h1 class="brand">${mark}<span>Bookmark<em>Manager</em></span></h1>
<span class="topbar-actions">
<a class="ghost" href="/">Library</a> <a class="ghost" href="/">Library</a>
<button type="button" class="ghost" onclick="void 0">Log out</button> <button type="button" class="ghost" onclick="void 0">Log out</button>
</span>
</header> </header>
<nav class="navrow" aria-label="Admin pages">${nav}</nav>`; <nav class="navrow" aria-label="Admin pages">${nav}</nav>`;
} }
@@ -957,6 +975,8 @@ function perSiteTable() {
const lane = LANES.find(l => l.site === site.id); const lane = LANES.find(l => l.site === site.id);
const out = lane ? lane.out : { refused: 0, unreachable: 0, no_chapter: 0, unfetchable: 0, errors: 0 }; const out = lane ? lane.out : { refused: 0, unreachable: 0, no_chapter: 0, unfetchable: 0, errors: 0 };
const state = lane ? laneStateWord(lane) : ""; const state = lane ? laneStateWord(lane) : "";
const tone = lane ? laneStateTone(lane) : "";
const stateMark = state ? `<span class="mark ${tone}">${state}</span>` : "";
const n = (v) => `<span class="c-num">${v}</span>`; const n = (v) => `<span class="c-num">${v}</span>`;
if (variant === "A") { if (variant === "A") {
const facts = [ const facts = [
@@ -968,7 +988,7 @@ function perSiteTable() {
`no chapter ${out.no_chapter}`, `unfetchable ${out.unfetchable}`, `errors ${out.errors}`, `no chapter ${out.no_chapter}`, `unfetchable ${out.unfetchable}`, `errors ${out.errors}`,
].join(" · "); ].join(" · ");
return `<div class="pst"><span class="pst-site">${site.label}</span>` + return `<div class="pst"><span class="pst-site">${site.label}</span>` +
(state ? ` <span class="mark">${state}</span>` : "") + (state ? ` ${stateMark}` : "") +
`<div class="pst-facts">${facts}</div></div>`; `<div class="pst-facts">${facts}</div></div>`;
} }
if (variant === "B") { if (variant === "B") {
@@ -979,7 +999,7 @@ function perSiteTable() {
<span>${series.filter(s => s.checked === null && s.url).length}</span> <span>${series.filter(s => s.checked === null && s.url).length}</span>
<span>${series.filter(s => s.checked !== null && s.checked > STALE_MIN).length}</span> <span>${series.filter(s => s.checked !== null && s.checked > STALE_MIN).length}</span>
${n(out.refused)}${n(out.unreachable)}${n(out.no_chapter)}${n(out.unfetchable)}${n(out.errors)} ${n(out.refused)}${n(out.unreachable)}${n(out.no_chapter)}${n(out.unfetchable)}${n(out.errors)}
<span class="c-state">${state}</span> <span class="c-state ${tone}">${state}</span>
</div>`; </div>`;
} }
const facts = [ const facts = [
@@ -990,7 +1010,7 @@ function perSiteTable() {
`refused ${out.refused}`, `unreachable ${out.unreachable}`, `refused ${out.refused}`, `unreachable ${out.unreachable}`,
`no chapter ${out.no_chapter}`, `unfetchable ${out.unfetchable}`, `errors ${out.errors}`, `no chapter ${out.no_chapter}`, `unfetchable ${out.unfetchable}`, `errors ${out.errors}`,
].join(" · "); ].join(" · ");
return `<div class="pblk"><div class="p1">${site.label}${state ? `<span class="mark">${state}</span>` : ""}</div><div class="p2">${facts}</div></div>`; return `<div class="pblk"><div class="p1">${site.label}${stateMark}</div><div class="p2">${facts}</div></div>`;
}).join(""); }).join("");
if (variant === "B") { if (variant === "B") {
@@ -1015,8 +1035,15 @@ function laneStateWord(l) {
} }
return l.skip; return l.skip;
} }
/* tone: a lane in trouble renders danger; a deliberate pause stays patina */
function laneStateTone(l) {
if (l.skip === "refusing" || l.skip === "sidecar-down") return "bad";
if (l.skip === "" && l.due > 0 && l.checked === 0) return "bad";
if (l.skip === "paused") return "ok";
return "";
}
function pauseControls(l) { function pauseControls(l) {
if (l.skip === "paused") return `<a class="ghost" href="#" onclick="event.preventDefault()">Resume</a>`; if (l.skip === "paused") return `<span class="pausebar"><a class="ghost" href="#" onclick="event.preventDefault()">Resume</a></span>`;
return `<span class="pausebar"> return `<span class="pausebar">
<select aria-label="Pause duration"> <select aria-label="Pause duration">
<option>1h</option><option selected>6h</option><option>24h</option> <option>1h</option><option selected>6h</option><option>24h</option>
@@ -1026,6 +1053,8 @@ function pauseControls(l) {
} }
function laneRow(l) { function laneRow(l) {
const state = laneStateWord(l); const state = laneStateWord(l);
const tone = laneStateTone(l);
const stateMark = state ? `<span class="mark ${tone}">${state}</span>` : "";
const attn = l.skip === "refusing" || l.skip === "sidecar-down" || const attn = l.skip === "refusing" || l.skip === "sidecar-down" ||
(l.skip === "" && l.due > 0 && l.checked === 0); (l.skip === "" && l.due > 0 && l.checked === 0);
if (variant === "A") { if (variant === "A") {
@@ -1036,7 +1065,7 @@ function laneRow(l) {
<span class="lane-fact">ran ${age(l.ranAgo)}</span> <span class="lane-fact">ran ${age(l.ranAgo)}</span>
${l.gap ? `<span class="lane-fact">gap ${l.gap}s</span>` : ""} ${l.gap ? `<span class="lane-fact">gap ${l.gap}s</span>` : ""}
${l.clamped ? `<span class="mark">gap at floor</span>` : ""} ${l.clamped ? `<span class="mark">gap at floor</span>` : ""}
${state ? `<span class="mark">${state}</span>` : ""} ${stateMark}
<span class="lane-out">${seriesOutcomes(l)}</span> <span class="lane-out">${seriesOutcomes(l)}</span>
<span class="lane-ctrl">${pauseControls(l)}</span> <span class="lane-ctrl">${pauseControls(l)}</span>
</li>`; </li>`;
@@ -1048,12 +1077,12 @@ function laneRow(l) {
<span>${l.checked}</span> <span>${l.checked}</span>
<span>${l.gap ? l.gap + "s" : "—"}</span> <span>${l.gap ? l.gap + "s" : "—"}</span>
<span>ran ${age(l.ranAgo)}${l.clamped ? " · clamped" : ""}</span> <span>ran ${age(l.ranAgo)}${l.clamped ? " · clamped" : ""}</span>
<span class="c-skip">${seriesOutcomes(l)} ${state ? `· ${state}` : ""}</span> <span class="c-skip">${seriesOutcomes(l)} ${state ? `· <span class="${tone}">${state}</span>` : ""}</span>
<span class="c-ctrl">${pauseControls(l)}</span> <span class="c-ctrl">${pauseControls(l)}</span>
</div>`; </div>`;
} }
return `<div class="lblk${attn ? " attention" : ""}"> return `<div class="lblk${attn ? " attention" : ""}">
<div class="l1">${l.site}${state ? `<span class="mark">${state}</span>` : ""}</div> <div class="l1">${l.site}${stateMark}</div>
<div class="l2"> <div class="l2">
<span>${l.due} due</span><span>${l.checked} checked</span> <span>${l.due} due</span><span>${l.checked} checked</span>
<span>ran ${age(l.ranAgo)}</span>${l.gap ? `<span>gap ${l.gap}s</span>` : ""} <span>ran ${age(l.ranAgo)}</span>${l.gap ? `<span>gap ${l.gap}s</span>` : ""}
@@ -1076,8 +1105,9 @@ function lanesPage() {
<p class="setup-copy">One row per Lane. Refreshes every 30s. The five counts <p class="setup-copy">One row per Lane. Refreshes every 30s. The five counts
are sums over the last 12h; a zero renders as <i>none observed</i> — an are sums over the last 12h; a zero renders as <i>none observed</i> — an
adapter reading a wrong number is not in this taxonomy and never can be.</p> adapter reading a wrong number is not in this taxonomy and never can be.</p>
<p class="setup-copy">Browser sidecar: <span class="mark">reachable</span> —
kagane, comix and novelfull poll through it; without it those lanes skip.</p>
${list} ${list}
<p class="setup-copy" style="padding-top:14px">Browser sidecar: <span class="mark">reachable</span></p>
</main>`; </main>`;
} }
@@ -1111,7 +1141,7 @@ function readersPage() {
/* Series list */ /* Series list */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function seriesRow(s) { function seriesRow(s) {
const marks = rowMarks(s).map(m => `<span class="mark">${m}</span>`).join(""); const marks = noteChips(s);
const metaFacts = [ const metaFacts = [
`<span class="${siteCls(s.site)}" style="color:var(--${s.site})">${siteLabel(s.site)}</span>`, `<span class="${siteCls(s.site)}" style="color:var(--${s.site})">${siteLabel(s.site)}</span>`,
`<span>ch ${s.chLabel}</span>`, `<span>ch ${s.chLabel}</span>`,