Bootstrap the Scholarr rewrite

This commit is contained in:
JustinZeus 2026-07-22 22:28:15 +02:00
commit 55820b3458
50 changed files with 3924 additions and 0 deletions

375
design/DESIGN.md Normal file
View file

@ -0,0 +1,375 @@
# Scholarr — Design Contract (DESIGN.md)
This is the binding design specification for the Scholarr web app. It is written
for the engineering team building the Vue 3 SPA. Everything visual in the app is
expressed through the tokens in `tokens.css`; every screen is a composition of the
named components below. If an implementation choice is not covered here, prefer the
option that is calmest, most honest about data, and easiest for a non-technical
seventy-year-old to understand.
The working reference implementation is `Scholarr.dc.html` (all pages, all states,
light/dark, responsive). This document is the contract; the file is the proof.
---
## 1. Product principles that shape the UI
1. **Honesty about sources is a feature.** Never fake real-time. Show "last delivered"
facts and set the expectation that data lags reality by days to weeks. Every source
has a visible health state.
2. **Identity is probabilistic.** Matching a followed name to a database identity has a
confidence level. Ambiguous cases go to a human review queue — a first-class daily
surface, not an admin afterthought.
3. **Nothing destructive by default.** Bulk actions preview and count before they act;
every consequential decision is undoable.
4. **Two audiences, one app.** The founding user is a retired academic who wants to know
"anything new for me?" in one glance. The admin wants dense, clutter-free control.
The Dashboard belongs to the reader; Sources/Activity/Settings belong to the admin.
5. **Self-hostable and private.** System-font stacks only; no CDN, no externally loaded
fonts, no third-party assets.
---
## 2. Signature & aesthetic
- **Concept: "the living bibliography."** The reading surface is typeset like a reference
list — serif titles, monospace identifiers (DOIs, arXiv IDs), a measured reading column.
- **Signature element:** the mortarboard-with-descending-binary logo (bundled as
`logo-light-tight.png` / `logo-dark-tight.png`), echoed by the honest **"as of" / "last
delivered" freshness stamps** that appear anywhere data is shown.
- **Type roles:** serif for titles & publication entries; sans for all chrome/UI; mono for
identifiers, timestamps, counts and uppercase micro-labels. Never set body UI in serif.
- **Restraint:** at most two background tones per screen. The binary motif appears only in
the logo — never as decoration.
---
## 3. Color tokens
Authored in oklch (see `tokens.css`); sRGB hex below for tools that need it. Semantic
colors are shared by two vocabularies: **matching confidence** and **source health**.
| Token | Role | Light | Dark |
|---|---|---|---|
| `--paper` | app background | `#fcfaf6` | `#161719` |
| `--surface` | cards, panels | `#fffefc` | `#1f2123` |
| `--surface-2` | insets, hover, skeleton | `#f6f3ee` | `#2b2d30` |
| `--ink` | primary text | `#29231d` | `#e9ebee` |
| `--ink-soft` | secondary text | `#5d5751` | `#afb1b4` |
| `--ink-faint` | meta / disabled | `#8b8580` | `#7e8084` |
| `--line` | hairline borders | `#e1ddd9` | `#35373a` |
| `--line-strong` | control borders | `#cbc6c1` | `#505357` |
| `--accent` | primary action fill | `#2e6799` | `#6aaae6` |
| `--accent-soft` | tinted backgrounds | `#dceeff` | `#1f3654` |
| `--accent-ink` | accent text | `#055085` | `#98cdff` |
| `--hi` / `--hi-soft` | **healthy / confident** | `#519160` / `#d9f3dd` | `#6cc185` / `#193d2f` |
| `--mid` / `--mid-soft` | **cooling down / likely** | `#bd8630` / `#fce9c6` | `#deaf56` / `#46381a` |
| `--low` / `--low-soft` | **failing / uncertain** | `#c0453f` / `#ffe0d8` | `#f47c6e` / `#562e27` |
Rules: text on `--paper`/`--surface` uses `--ink`/`--ink-soft`. Solid `--accent`/`--hi`/
`--mid`/`--low` fills always pair with near-white text (`oklch(0.99 0 0)`) — verify AA.
`-soft` tokens are backgrounds only; pair with the matching solid token for text.
---
## 4. Typography
| Role | Font | Size | Weight | Notes |
|---|---|---|---|---|
| Page title (H1) | serif | `clamp(21px, 4vw, 27px)` | 700 | letter-spacing 0.015em |
| Section / card title | serif | 1517px | 700 | |
| Publication title | serif | 14.516.5px | 700 unread / 500 read | `text-wrap: pretty` |
| Body | sans | 1314px | 400500 | line-height 1.45 |
| Meta / secondary | sans | 11.512.5px | 400 | `--ink-soft` |
| Identifiers, timestamps, counts | mono | 10.511.5px | 400700 | `--ink-faint` |
| Uppercase micro-label | mono | 10px | 700 | letter-spacing 0.080.12em |
Minimum body text 12px. Titles truncate to one line with ellipsis in dense lists; wrap in
detail/feed views.
---
## 5. Space, radius, elevation
- **Space scale:** 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40. Lay groups out with flex/grid + `gap`.
- **Radius:** `--r-sm` 6 (controls), `--r-md` 10 (option cards), `--r-lg` 16 (panels), `--r-pill` 20 (chips/badges).
- **Elevation:** `--sh-1` resting cards; `--sh-2` overlays (mobile bars, modals, toasts). Borders (`--line`) do most of the separation work; shadows are subtle.
- **Reading column:** dashboard ≤ 780px, publications/authors ≤ 1100px, detail/review/forms ≤ 820900px, centered.
---
## 6. The two badge vocabularies
Both render as a pill: `--r-pill`, mono 1011px 700, `-soft` background + solid-token text,
optional leading dot. **These words are the end-user language — do not show raw scores or
internal states in the UI.**
### Matching confidence (author identity)
| Internal | Badge label | Token | When |
|---|---|---|---|
| high (≥ ~90%) | **Confident** | `--hi` | Identifiers + works agree across sources |
| medium | **Likely** | `--mid` | Probable, but the name is shared — worth a look |
| low | **Uncertain** | `--low` | Weak evidence; review before trusting |
| shell | **Not yet identified** | `--ink-faint` | A name with no database identity |
### Source health
| Internal | Badge label | Token | Plain-language meaning |
|---|---|---|---|
| healthy | **Healthy** | `--hi` | Delivering normally |
| cooling | **Cooling down** | `--mid` | We asked too fast; the source told us to slow down. Resumes on its own. |
| failing | **Not connected** | `--low` | Sign-in/credentials failed; matching continues from other sources |
### Open-access availability (per publication)
`◈ OA` (`--hi`) available · `◇ ?` (`--ink-faint`) checking Unpaywall · `⊘` (`--low`) no free PDF found.
---
## 7. Component inventory (with states)
Every component below is component-shaped: repeated instances are structurally identical,
styled only through tokens, translatable 1:1 to a scoped Vue component.
- **Buttons***primary* (`--accent` fill, near-white text), *secondary* (`--surface` +
`--line-strong` border), *ghost* (transparent, `--ink-soft`), *destructive-lite*
(`--low` text on `--surface`). States: default / hover (`--surface-2` or darker accent) /
focus-visible (2px `--accent` ring) / disabled (`--ink-faint`, dashed border for
unavailable actions like a missing PDF) / active-pressed.
- **Inputs & selects**`--surface-2` fill, `--line-strong` border, label in `--ink-soft`
12px. Focus ring as global. (Static in mockups; real fields in Vue.)
- **Filter chips / segmented control** — pill, active = `--accent-soft` bg + `--accent-ink`
text + `--accent` border; inactive = `--surface` + `--line`. Optional mono count sub-pill.
- **List row (publication)** — `[unread dot] [field tag · serif title (truncate)] [meta line:
authors · venue · year · id · source] [NEW marker slot] [OA badge] [PDF button] [read toggle]`.
Unread: filled `--unread` dot with `--accent-soft` halo, title 700. Read: hollow dot,
title 500, row opacity 0.66. Hover: `--surface-2`.
- **List row (author)** — `[checkbox] [avatar] [name (+ transliteration)] [field] [confidence
badge] [last sync] [new count]`. Selectable; header row labels the columns.
- **Table header** — mono uppercase micro-labels, `--line` bottom border.
- **Cards / panels**`--surface`, `--line` border, `--r-lg`, `--sh-1`; header row with serif
title + optional mono meta, hairline-separated body.
- **Avatar** — circle, initials, `--surface-2` (neutral) or `--accent-soft` (self/active).
- **Badges** — confidence / source-health / OA / role, per §6.
- **Freshness stamp** — mono `--ink-faint`, always phrased "as of …" or "last delivered …".
- **Progress bar** — 6px track `--surface-2`, `--accent` fill (review queue, bulk import).
- **Modal / overlay**`--surface`, `--sh-2`, `--r-lg`; dim scrim. (Pattern; compose as needed.)
- **Toast**`-soft` bg + matching solid icon + `--sh-2`; auto-dismiss, carries an Undo action
for consequential changes.
- **Empty state** — centered glyph + serif headline + one sentence of `--ink-soft` guidance +
one primary action. Never a dead end.
- **Skeleton**`--surface-2``--line` shimmer, respects reduced-motion (freezes). Mirrors the
real row's shape and count.
- **Bottom tab bar (mobile)** — fixed, `--surface`, `--sh-2`, 5 destinations, badge dots.
### Required states per page
Populated, **empty**, **loading (skeleton)**, and **degraded** are demonstrated in the
reference file. Degraded truths that must remain visible: a cooling-down source (Unpaywall),
a not-connected source (ORCID), an unresolvable PDF, an author shell, and a
three-plausible-candidates review case.
---
## 8. Interaction patterns
- **Navigation** — persistent collapsible left sidebar on desktop; a fixed bottom tab bar on
phone. The nav must never scroll horizontally (v1's bug). Below 760px the sidebar is replaced,
not shrunk.
- **Theme** — system / light / dark switch. System follows `prefers-color-scheme`; explicit
choice sets `data-theme` on the root.
- **Bulk selection** — row checkboxes reveal a selection bar with count + non-destructive actions
(re-sync, send to review) + Clear. Nothing acts without an explicit button.
- **Keyboard triage (Review queue)** — one decision at a time. `1`/`2`/`3` choose a candidate,
`S` skip, `U` undo, `J`/`K` (or ↑/↓) move. Visible `<kbd>` hints. Every decision is reversible.
- **Undo** — bulk imports and review decisions are undoable; imports can be reversed from Activity.
- **Freshness** — show relative times ("2h ago") plus absolute where precision matters. Never imply
live data.
---
## 9. Copy rules & phrase glossary
- Address the reader plainly and warmly; avoid jargon and internal terms.
- **No em dashes.** Use periods, commas, or a middot (·) separator.
- Numbers and identifiers in mono; prose in sans.
- Never say "scraped", "crawler", "confidence score 0.71", "rate-limited (HTTP 429)". Say the
plain-language equivalent.
| Say this | Not this |
|---|---|
| Confident match / Likely / Uncertain | 0.98 / 0.71 / 0.48, high/med/low |
| Not yet identified (author shell) | null identity, orphan record |
| Cooling down — resumes on its own | rate-limited, HTTP 429, backoff |
| Not connected — sign-in was rejected | auth failure 401 |
| No free PDF found | Unpaywall miss |
| open-access PDF | OA green/gold, self-archived |
| last delivered 2h ago / as of … | live, real-time, up to the second |
| We will keep checking on schedule | polling, cron |
---
## 10. Accessibility
- **Contrast:** WCAG AA for all text and meaningful UI. Verify solid-fill + near-white text and
every `-soft`/solid pairing in both themes.
- **Focus:** visible 2px `--accent` ring on every interactive element (`:focus-visible`).
- **Reduced motion:** `prefers-reduced-motion` freezes skeleton shimmer and all transitions.
- **Color is never the only signal:** confidence and health always pair the color with a word
(and often a dot/icon); unread/read pair color with dot fill and weight.
- **Targets:** ≥ 44px hit targets on mobile controls.
- **Semantics:** real headings, buttons for actions, labels tied to inputs; transliterated names
carry the Latinized form alongside.
---
## 11. Vue 3 mapping notes
- Ship `tokens.css` once at the app root; never hard-code a color, font, radius, or shadow —
reference a `var(--token)`. Theme switching only toggles `data-theme` on the root.
- Each component in §7 becomes one scoped SFC. Keep markup identical across repeated instances
(the reference file already does this) so the port is mechanical.
- Derived styles that depend on data (confidence color, read/unread weight, active chip) should
be computed props returning token names, not literal colors.
- No technique in the reference relies on anything Vue-hostile: no global class cascade beyond
tokens + a few resets, no runtime style injection, no third-party fonts or assets.
---
## 3b. Contrast proof (WCAG AA)
Measured contrast ratios for every token pairing used for **text** (AA needs 4.5:1 for body, 3:1 for large). Ratios ≥ 4.5 unless noted.
New text-safe tokens added this round: `--hi-ink` / `--mid-ink` / `--low-ink` (AA text on the matching `-soft` background), and `--on-accent` (text on solid fills — flips near-white in light, near-black in dark).
| Pairing | Light | Dark |
|---|---|---|
| `--ink` on `--surface` | 15.4 | 13.5 |
| `--ink-soft` on `--surface` | 7.1 | 7.5 |
| `--ink-faint` on `--surface` | 4.8 | 5.2 |
| `--accent-ink` on `--surface` | 8.4 | 9.6 |
| `--accent-ink` on `--accent-soft` | 7.1 | 7.3 |
| `--hi-ink` on `--hi-soft` | 6.2 | 7.9 |
| `--mid-ink` on `--mid-soft` | 6.4 | 7.4 |
| `--low-ink` on `--low-soft` | 6.0 | 6.3 |
| `--on-accent` on `--accent` (primary btn) | 5.8 | 7.7 |
| `--on-accent` on `--low` (destructive btn) | 4.9 | 7.2 |
Rule that made the earlier "Likely / Cooling down" badge (2.66:1) pass: **solid semantic colours (`--hi`/`--mid`/`--low`) are never used as badge text.** Badge text is always the `-ink` variant on the `-soft` background. Solid fills only ever carry `--on-accent` text, and only for `--accent`/`--low` (never `--hi`/`--mid`, whose light-theme solids fail with white). `--ink-faint` was darkened to clear AA on `--surface`.
## 12. Motion & interaction spec
Tokens (in `tokens.css`): `--dur-fast` 120ms, `--dur-base` 180ms, `--dur-slow` 260ms; `--ease-out` cubic-bezier(.2,0,0,1), `--ease-in-out` cubic-bezier(.4,0,.2,1).
- **Animates:** hover/focus colour transitions (fast); toggle-switch knob (base); filter/theme chip selection (fast); skeleton shimmer (1.4s loop); toast slide-up + fade (base); modal scrim fade + card rise (base); tooltip fade+rise (fast).
- **Never animates:** content arriving from a sync (rows appear without shifting neighbours — reserve space with skeletons); route/screen changes (instant, no cross-fade — this is a data tool); numbers/counts (no count-up); the reading list on mark-read (opacity change only, no reflow/animation of position).
- **Reduced motion** (`prefers-reduced-motion: reduce`, handled globally in tokens.css): all transitions/animations collapse to ~0ms; skeleton shimmer freezes to a static tint; tooltips and toasts appear/disappear instantly; no scroll animation.
**Keyboard triage (Review queue)** — one decision on screen at a time:
| Key | Action |
|---|---|
| `1` `2` `3` | Choose candidate 1/2/3 |
| `S` | Skip (decide later) |
| `U` | Undo last decision |
| `J` / `↓` | Next item |
| `K` / `↑` | Previous item |
Every decision is reversible; the queue shows progress (`n of N`) and an always-available Undo. Nothing is committed to the shared identity graph until the user decides.
**Bulk selection** (Authors): row checkboxes reveal a selection bar with a live count and only non-destructive actions (Re-sync, Send to review) plus Clear. No action fires without an explicit button press; destructive operations route through the confirm modal (§17).
## 13. Responsive spec
Breakpoint: **phone < 760px**, **wide ≥ 760px** (single breakpoint by design — the tool is comfortable on a 13" laptop, usable on a phone). Documented as `--bp-phone: 760px`; because custom properties can't drive `@media`, the Vue app defines the query once in a composable and the value must match this token.
Per-surface reflow:
- **Navigation:** wide = persistent left sidebar (collapsible to 64px icon rail); phone = fixed **bottom tab bar** + a slim top bar. The nav never scrolls horizontally (the v1 bug). Below 760 the sidebar is *replaced*, not shrunk.
- **Dashboard:** hero + feed stack to one column; the "new work from" author chips wrap.
- **Publications:** rows keep their shape; the meta line truncates with ellipsis; filter chips scroll inside their own contained track (never the page).
- **Authors table:** columns drop progressively on phone — keep Author + Identity(confidence) + New; hide Field and Last-sync (available in author detail). The row stays the same component, columns toggle via the breakpoint.
- **Author detail / Add / Review / forms:** two-column layouts collapse to one; the identity panel moves below the publications list.
- **Modal:** full-width minus 20px margin on phone; **toast** spans near-full width, bottom, above the tab bar.
- Reading-column caps: dashboard 780, publications/authors 1100, detail/sources/activity 900, review/add/forms 820.
## 14. Form rules
- **Labels above inputs**, 12px `--ink-soft` 600. Required fields marked with `*` in `--low-ink` and stated once ("Fields marked * are required").
- **Validation on save**, not per keystroke (calm for the founding user). On failure: input gets `--low` border, an inline message below the field (`--low-ink`, 11.5px, leading `warning` icon), AA-legible; focus moves to the first invalid field.
- **Submit states:** default "Save changes" → disabled "Saving…" → success shows an inline "saved" banner (`--hi-soft`/`--hi-ink` + check) AND a toast. Cancel reverts to the read-only view with no change.
- Read-only view shows values as plain text (not disabled inputs); an explicit **Edit** button enters the form. This mirrors the Settings → Profile states (view / editing / error / saving / saved).
## 15. Copy glossary (complete — one term per concept)
Confidence: **Confident** · **Likely** · **Uncertain** · **Not yet identified** (shell).
Source health: **Healthy** · **Cooling down** · **Not connected**.
Open access: **open-access PDF** (available) · **checking** (Unpaywall resolving) · **no free PDF** (none found).
Freshness (always relative + honest, never "live"): **"last delivered {t}"**, **"as of {t}"**, **"last checked {t}"**, **"Up to date · {t}"**, **"arXiv can run a day or two behind. That is normal."**
Review actions: **Skip for now** · **Undo** · **None of these** / **Reject match** / **Keep separate** / **Keep both** / **Leave as shell** (reject label varies by case) · **Merge** (duplicates).
Bulk import: **"We checked your {n} lines before importing anything"**, **"look valid"**, **"could not be read"**, **"duplicates within your list"**, **"already in your followed list"**, **"Follow {n} confident matches"**, **"Send {n} to review"**, **"Safe by default: nothing is followed until you confirm."**
Empty states (one line each): Publications "Nothing to read yet." · Authors "You are not following anyone yet." · Author detail "No publications from this author are in your library yet." · Review "Queue is clear." · Sources "No sources connected yet." · Dashboard feed "You are all caught up."
Error patterns: **404** "That page is not on the shelf. … Nothing is broken." · Login fail "That username or password was not recognised. Please try again." · Field "Enter a valid email address, for example name@example.org." · Source fail "Not delivering. Sign-in or credentials failed; the other sources keep working."
Destructive confirm: **"Remove {name}?"** + what is and isn't affected + **"Remove user"** / **Cancel**; success toast **"{name} was removed. Undo?"**
Never shown to users: raw scores/percentages as the label, "scraped", "crawler", "rate-limited/429", "auth 401", "polling/cron", "real-time".
## 16. Icon set
All icons: 24×24 viewBox, `fill:none; stroke:currentColor; stroke-width:1.8; round caps/joins`. Shipped as `assets/icons/{name}.svg` and as an inline `<symbol id="ic-{name}">` sprite. Inherit colour from context; size via width/height.
`dashboard` (nav: home/overview) · `publications` (nav: reading list) · `authors` (nav: followed people) · `review` (nav: identity triage / balance) · `sources` (nav: data providers / database) · `activity` (nav: sync history) · `settings` (nav: gear) · `search` · `check` (read / confirmed / valid) · `circle` (unread-toggle empty / read-only) · `dot` (filled status dot) · `plus` (add) · `external` (outbound link / open PDF in new tab) · `chev-left` / `chev-right` (collapse / paginate) · `arrow-left` (back) · `undo` · `sun` / `moon` / `system` (theme) · `signout` · `warning` (caution / validation) · `x` (close / remove) · `oa` (open padlock = open-access available) · `lock` (closed padlock = no free PDF) · `pending` (dashed circle = checking) · `book` (empty reading state) · `upload` (bulk import) · `eye` (colour-blind toggle).
Typographic characters kept intentionally (NOT icons): `→` `←` `·` in prose/buttons. No em dashes anywhere.
## 17. Global patterns
- **Toast + undo:** bottom-centre, inverse surface (`--ink` bg / `--paper` text), one at a time, auto-dismiss 5s, always offers **Undo** for consequential changes. New toast replaces the current one.
- **Modal / confirm dialog:** scrim (`oklch(.2 .02 60/.5)`) + centred `--surface` card, `--sh-2`, title (serif) + body + footer actions right-aligned (Cancel secondary, destructive uses `--low` fill + `--on-accent`). Used for every destructive action (delete user, remove author with follow-data, reconnect source).
- **404:** full-bleed, in the product's calm voice, reassures nothing is broken, single primary action back to the dashboard.
- **Reader (non-admin) view:** admin-only surfaces are **hidden, not disabled** — Sources and Activity drop out of the nav; Users & roles, Sign-in and Source-credentials sections drop out of Settings. Toggle via "View as" (demo affordance; in production the role comes from the account).
- **Tooltips:** hover **and** keyboard-focus reveal (`.tip`/`.tip-pop`), used to explain status badges in place (replaces a static legend). Instant under reduced motion.
- **Colour-blind mode:** adds solid/dashed/dotted outlines to status badges so the three-stop scale is distinguishable without hue; toggle in the sidebar and in Settings → Accessibility; persisted in `localStorage` (`scholarr-cb`). Colour is never the only signal even with the mode off (every badge carries a word).
## 18. Build note — support.js is tool-only
The reference implementation `Scholarr.dc.html` loads a small runtime (`support.js`) that belongs to **the design tool only**. It is **not part of the production build and must not be shipped**. The Vue app reproduces the same markup and behaviour natively; every page must render correctly with no `support.js` present. The only runtime assets are `tokens.css`, the icon SVGs, the logos and the favicons.
## 19. Vue handoff appendix
**Theming strategy (pick one, this is it):** attribute strategy — `data-theme` on the root element, with `prefers-color-scheme` as the default when the attribute is absent (exactly as `tokens.css` is written). No component sets a raw colour; **all** colour/space/radius/shadow/motion flows through `var(--token)`. Scoped Vue `<style>` blocks reference tokens only.
**Component tree & responsibilities:**
- `AppShell` — CSS grid, owns theme + role + colour-blind state; slots `PrimaryNav`, the active view, `MobileTabBar`, `ToastHost`, `ModalHost`.
- `PrimaryNav` (sidebar; `collapsed` prop) → `NavItem`(icon, label, badge, active), `ThemeSwitch`(model system|light|dark), `ColorblindToggle`, `RoleSwitch`, `UserCard`.
- `MobileTabBar``TabItem`(icon, label, badge).
- Views (one per route): `DashboardView`, `PublicationsView`, `AuthorsView`, `AuthorDetailView`, `ReviewView`, `AddAuthorView`, `BulkImportView`, `SourcesView`, `ActivityView`, `SettingsView`, `LoginView`, `OnboardingView`, `NotFoundView`.
- Shared components (props → slots):
- `PageHeader`(title, meta, actions-slot)
- `FreshnessStamp`(kind: last-delivered|as-of|last-checked, time)
- `ConfidencePill`(level: high|medium|low|shell) — renders label + tooltip; reads colour-blind flag for outline.
- `SourceHealthBadge`(state: healthy|cooling|failing) and `SourceHealthCard`(source) with reconnect action.
- `OaBadge`(status: pdf|pending|none) — icon + label + tooltip.
- `PubRow`(pub, read, isNew; emits open-pdf, toggle-read) — the bibliography-style entry.
- `AuthorRow`(author, selected; emits toggle-select, open).
- `FilterChip`(label, count, active), `StatePreviewToggle` (dev-only, remove in prod).
- `Skeleton`(shape: row|card, count), `EmptyState`(icon, title, body, action-slot).
- `Tooltip` (`.tip`/`.tip-pop`; hover + focus).
- `Modal` / `ConfirmDialog`(title, body, confirmLabel, destructive) — emits confirm/cancel.
- `Toast`(message, undo) via `ToastHost`.
- `Card`, `Button`(variant: primary|secondary|ghost|danger; disabled), `Field`(label, required, error), `ToggleSwitch`(checked), `Avatar`(initials, tone).
- `TriageDeck` (Review) — current item + keyboard handling + progress + undo.
- `CandidateCard` (Add author disambiguation), `BulkStepper` (import phases).
Repeated markup is already identical instance-to-instance in the reference file, so each maps to one component with no structural surprises.
## 20. Asset finalization
- **Logos:** `assets/logo-light.png` (dark mark, for light backgrounds) and `assets/logo-dark.png` (light mark, for dark backgrounds). The app swaps by effective theme. Minimum display 24px; clear space ≥ 25% of the mark's height on all sides; never recolour, rotate, or stretch — the descending-binary tail is part of the mark and the signature.
- **Favicons / PWA:** `assets/favicon.png` and `assets/favicon-512.png` (maskable-safe square).
- **Icons:** `assets/icons/*.svg` (29 files) plus the inline sprite; `currentColor`, 24 viewBox, 1.8 stroke — see §16.
## 21. Deliverables in this package
- `Scholarr.dc.html` — the living reference implementation: all 13 screens, every state (populated / loading / empty / degraded), light + dark, responsive, colour-blind mode, working review triage, confirm modal, undo toast, 404. (Runs on the tool's support.js — see §18; not shipped.)
- `component-reference.html` — every component in its states, both themes, self-contained (links `tokens.css`, own stylesheet, vanilla-JS theme/colour-blind toggles). Open it directly.
- `tokens.css` — the frozen token layer (colour incl. `-ink`/`--on-accent`, type, space, radius, elevation, motion).
- `DESIGN.md` — this contract.
- `assets/` — icons, logos, favicons.