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

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Editors and operating systems
.DS_Store
.idea/
.vscode/
# Local configuration and secrets
.env
.env.*
!.env.example
*.local.yaml
*.local.yml
# Go
/bin/
/dist/
coverage.out
*.test
# Vue and Node
node_modules/
frontend/dist/
*.tsbuildinfo
# Runtime data
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3
/data/
/backups/
# Temporary files
*.log
*.tmp
.cache/

95
AGENTS.md Normal file
View file

@ -0,0 +1,95 @@
# Scholarr agent brief
This file is the canonical brief for every coding agent working in this repository. Read it and
`TASKS.md` before making changes.
## Product
Scholarr is a public, self-hosted FOSS author watchlist and publication tracker. A user follows
authors, learns when they publish, and finds legal open-access PDFs. It must serve a real
multi-user household deployment while remaining straightforward for the wider self-hosting
community.
## Immutable doctrine
There is zero Google Scholar network contact, ever.
- No application code, test, tool, CI job, or documentation example may request a Google
endpoint.
- No scraping, browser automation, CAPTCHA handling, anti-bot behavior, or evasion machinery.
- Scholar IDs and URLs may exist only as inert imported metadata and human-clickable links.
- Acquisition uses documented APIs: OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
- Semantic Scholar is not a v1 requirement.
- Provider clients must use a contact identity, conservative persisted rate limits, restart-safe
cooldowns, and user-visible health. OpenAlex must stay at or below 2 requests per second.
This doctrine outranks convenience and implementation shortcuts.
## Locked architecture
- Backend: Go.
- Frontend: Vue.
- Database: SQLite only. Design for single-writer discipline from the start.
- Root author entity: `FollowedAuthor`, with one or more `AuthorSourceIdentity` records.
- Names are labels, never identity keys. External source IDs anchor identity.
- Publications are globally deduplicated. Follows and read state are per user.
- Auth v1 includes internal username/password, generic OIDC, and trusted-header/forward-auth.
- The service is config-driven. One declarative config controls providers and behavior.
- GitHub `justinzeus/scholarr` is the intended public primary. Forgejo keeps a copy.
Do not silently change a locked decision. Record a proposed change as open and ask the owner.
## Decision record
On tank, the authoritative project runbook is:
`/opt/stacks/server-docs/runbooks/scholarr-rewrite/README.md`
It owns doctrine, locked decisions, empirical calibration results, and project state. Its
companion files own the identity model and testing strategy. This brief is self-contained for
agents without tank access, but any state or design decision reached on tank must also be written
to that runbook in the same session.
## Frozen UI contract
- `design/DESIGN.md` is the frozen Vue handoff contract.
- `design/tokens.css`, `design/component-reference.html`, and `design/assets/` are canonical UI
inputs.
- `design/reference/Scholarr.dc.html` is the reviewed proof implementation.
- `design/reference/support.js` belongs only to that design proof. Never ship it in production.
- Port the design faithfully. Do not redesign while implementing.
- Carry two cleanup items into the Vue port: avoid em dashes in bulk-import copy, and do not show
raw match percentages as user-facing confidence labels.
UI QA is manual and performed by the owner against the frozen contract. Do not add or run
Playwright, Selenium, or other browser automation. Unit, integration, build, and curl smoke tests
are expected.
## Work process
- Work through `TASKS.md` in order. Each task has a goal and acceptance criteria.
- Freeze one spec at a time. Decided items are labeled decided; open items remain open for owner
review.
- Do not start implementation for an area whose spec is not frozen.
- Keep commits focused and use plain imperative commit messages without trailers.
- Stage only files you changed.
- Do not use em dashes in prose, code comments, commit messages, or user-facing copy.
- Never commit secrets, real user data, database dumps, provider payloads containing private
data, API keys, tokens, or real email addresses.
- Keep configuration examples synthetic and safe to publish.
## Correctness obligations
- Pure normalization, fingerprinting, parsing, and scoring logic gets table tests, property tests
for invariants, and Go fuzz tests for external input parsers.
- Provider clients use deterministic `httptest` fixtures, including 429 responses, missing
`Retry-After`, truncated JSON, and empty pages. Rate and cooldown tests use a fake clock.
- Sync tests prove one active run per user, idempotent reruns, additive-only baseline import,
resumability after interruption, and SQLite concurrency behavior. Run the race detector.
- Every migration is tested up and down, including upgrade from a seeded prior-version database.
- End-to-end smoke boots a seeded SQLite database with stub providers and exercises onboarding,
import, sync, review, read state, and export through the API.
- Matching and dedup changes may not regress the recorded golden-corpus baseline.
Dry-run import previews and dry-run destructive admin actions are product features, not test-only
helpers.

1
CLAUDE.md Normal file
View file

@ -0,0 +1 @@
Read AGENTS.md.

36
README.md Normal file
View file

@ -0,0 +1,36 @@
# Scholarr
Scholarr is a self-hosted author watchlist and publication tracker. Follow researchers, learn
when they publish, and find legal open-access copies of their work.
The rewrite is pre-alpha. Its product direction and interface are settled, and the architecture
specs are being frozen before implementation begins.
## Non-negotiable rule
Scholarr never makes network requests to Google Scholar or any other Google endpoint. Imported
Scholar IDs and URLs are inert metadata and human-clickable links only. Publication data comes
from documented APIs such as OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
## Technical direction
- Go backend
- Vue frontend
- SQLite only
- Multi-user, with a globally deduplicated publication store and per-user follow/read state
- Internal auth, generic OIDC, and trusted-header auth in v1
- Config-driven operation and simple first-run onboarding
See [AGENTS.md](AGENTS.md) for the full project constraints and [TASKS.md](TASKS.md) for the
ordered work queue.
## Frozen interface contract
The reviewed interface contract is in [design/DESIGN.md](design/DESIGN.md). Its tokens, component
reference, icons, logos, favicons, and proof implementation are checked in beside it. These files
are the source for the Vue port, not an application implementation.
## License
The license has not been selected yet. Until a LICENSE file is added, normal copyright rules
apply. License selection is an explicit project decision in [TASKS.md](TASKS.md).

89
TASKS.md Normal file
View file

@ -0,0 +1,89 @@
# Scholarr task cards
Work in order. A card is complete only when its acceptance criteria are met. Specs are reviewed
and frozen one at a time before implementation begins.
## Card 0: publish the clean repository **IN PROGRESS**
**Goal:** publish this clean-history bootstrap as the public project.
The local bootstrap is complete and the personal GitHub identity is authenticated on tank. Publish
this same history to GitHub first, then to the Forgejo copy.
**Acceptance:** public `justinzeus/scholarr` repositories exist on GitHub (primary) and Forgejo
(copy), both point at this clean history, no legacy history is present, and no secret or work
identity appears anywhere.
## Card 1: freeze the data model spec
**Goal:** specify global author/publication identity, per-user follows/read state, review records,
and merge/undo behavior.
Inputs: the project identity decision record, the legacy schema concepts worth salvaging, and the
global `FollowedAuthor` plus `AuthorSourceIdentity` direction.
**Acceptance:** `docs/specs/data-model.md` is owner-reviewed and marked frozen; constraints,
migrations, duplicate prevention, audit/undo behavior, and every remaining open question are
explicit.
## Card 2: freeze provider contracts and health
**Goal:** specify sanctioned API contracts, etiquette, persisted rate clocks/cooldowns, and
user-visible source health for OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
**Acceptance:** `docs/specs/providers.md` is owner-reviewed and marked frozen; keyed and anonymous
lanes, contact identity, retry semantics, restart behavior, fixture boundaries, and open questions
are explicit.
## Card 3: freeze the sync engine and run lifecycle
**Goal:** define safe, resumable acquisition within SQLite's concurrency model.
**Acceptance:** `docs/specs/sync-engine.md` is owner-reviewed and marked frozen; it defines one
active run per user, idempotent reruns, additive-only baseline import, resumability, writer
serialization, dry runs, and failure recovery.
## Card 4: freeze PDF resolution
**Goal:** define legal open-access discovery and provenance.
**Acceptance:** `docs/specs/pdf-resolution.md` is owner-reviewed and marked frozen; source order,
license/provenance handling, retry behavior, and the absence of all Google access are explicit.
## Card 5: freeze auth
**Goal:** define internal, generic OIDC, and trusted-header auth without provider-specific coupling.
**Acceptance:** `docs/specs/auth.md` is owner-reviewed and marked frozen; identity linking, roles,
session/security behavior, first-admin recovery, and mode-specific trust boundaries are explicit.
## Card 6: freeze the config schema
**Goal:** define one declarative configuration surface for the service.
**Acceptance:** `docs/specs/config.md` is owner-reviewed and marked frozen; defaults, validation,
secret references, provider settings, reload/restart behavior, and upgrade compatibility are
explicit.
## Card 7: freeze onboarding UX
**Goal:** make first run straightforward: search or paste Scholar URLs, review candidates, confirm,
and begin tracking.
**Acceptance:** `docs/specs/onboarding.md` is owner-reviewed and marked frozen; empty, ambiguous,
cooldown, partial-success, and unresolved-shell paths match the frozen UI and honest calibration
expectations.
## Card 8: choose the public license
**Goal:** make the repository genuinely open source under a consciously selected OSI license.
**Acceptance:** the owner selects the license, `LICENSE` is added with correct copyright identity,
and README/project metadata are updated.
## Card 9: plan build phases
**Goal:** split implementation into reviewable vertical slices against the frozen specs and UI.
**Acceptance:** build cards cover schema/migrations, providers, sync, auth, API, Vue port,
packaging, backup/restore, and release gates. Every card names its tests and demo proof.

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.

15
design/README.md Normal file
View file

@ -0,0 +1,15 @@
# Frozen UI reference
This directory is the reviewed Scholarr UI contract frozen on 2026-07-22.
- `DESIGN.md` is the canonical Vue handoff.
- `tokens.css` is the canonical token layer.
- `component-reference.html` renders the component library from those tokens.
- `assets/` contains the production icon, logo, and favicon inputs.
- `reference/Scholarr.dc.html` is the proof implementation used during design review.
- `reference/support.js` and `reference/uploads/` exist only to render that proof. The support
runtime must never be included in the production application.
The original reviewed files came from the git-excluded legacy design workspace on tank at
`/opt/stacks/scholarr/design/round2/`. Change the product UI only through an explicit owner-approved
design revision. Implementation work should port this contract, not edit it in place.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
design/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3 8 4-16 3 8h4"></path></svg>

After

Width:  |  Height:  |  Size: 229 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="20" y1="12" x2="5" y2="12"></line><path d="M11 6l-6 6 6 6"></path></svg>

After

Width:  |  Height:  |  Size: 265 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3"></circle><path d="M3.5 20c0-3.3 2.5-5.5 5.5-5.5s5.5 2.2 5.5 5.5"></path><circle cx="17.5" cy="9.5" r="2.3"></circle><path d="M16 15c2.4.2 4 2.1 4 5"></path></svg>

After

Width:  |  Height:  |  Size: 373 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5C4 4.7 4.7 4 5.5 4H11v16H5.5C4.7 20 4 20.5 4 21.3z"></path><path d="M20 5.5C20 4.7 19.3 4 18.5 4H13v16h5.5c.8 0 1.5.5 1.5 1.3z"></path></svg>

After

Width:  |  Height:  |  Size: 339 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"></path></svg>

After

Width:  |  Height:  |  Size: 226 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"></path></svg>

After

Width:  |  Height:  |  Size: 221 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"></path></svg>

After

Width:  |  Height:  |  Size: 219 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"></circle></svg>

After

Width:  |  Height:  |  Size: 228 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="18" rx="1.5"></rect><rect x="14" y="3" width="7" height="8" rx="1.5"></rect><rect x="14" y="15" width="7" height="6" rx="1.5"></rect></svg>

After

Width:  |  Height:  |  Size: 358 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="7" fill="currentColor" stroke="none"></circle></svg>

After

Width:  |  Height:  |  Size: 262 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6"></path><line x1="20" y1="4" x2="10.5" y2="13.5"></line><path d="M18 13.5V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5.5"></path></svg>

After

Width:  |  Height:  |  Size: 343 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"></path><circle cx="12" cy="12" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 294 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V7.5a4 4 0 0 1 8 0V11"></path></svg>

After

Width:  |  Height:  |  Size: 288 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13.5A8 8 0 1 1 10.5 4a6.3 6.3 0 0 0 9.5 9.5z"></path></svg>

After

Width:  |  Height:  |  Size: 255 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V7.5a4 4 0 0 1 7.3-2.3"></path></svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" stroke-dasharray="2.6 3"></circle></svg>

After

Width:  |  Height:  |  Size: 253 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 277 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="18" x2="14" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 319 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"></path><line x1="6" y1="7" x2="18" y2="7"></line><path d="M6 7l-3 6.5h6z"></path><path d="M18 7l-3 6.5h6z"></path><line x1="8" y1="21" x2="16" y2="21"></line></svg>

After

Width:  |  Height:  |  Size: 366 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="16.5" y1="16.5" x2="21" y2="21"></line></svg>

After

Width:  |  Height:  |  Size: 277 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1"></path></svg>

After

Width:  |  Height:  |  Size: 334 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2"></path><line x1="19" y1="12" x2="9" y2="12"></line><path d="M16 9l3 3-3 3"></path></svg>

After

Width:  |  Height:  |  Size: 355 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.8"></ellipse><path d="M5 5.5v13c0 1.6 3.1 2.8 7 2.8s7-1.2 7-2.8v-13"></path><path d="M5 12c0 1.6 3.1 2.8 7 2.8s7-1.2 7-2.8"></path></svg>

After

Width:  |  Height:  |  Size: 359 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M19.1 4.9l-1.8 1.8M6.7 17.3l-1.8 1.8"></path></svg>

After

Width:  |  Height:  |  Size: 356 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="1.5"></rect><line x1="8" y1="20" x2="16" y2="20"></line><line x1="12" y1="16" x2="12" y2="20"></line></svg>

After

Width:  |  Height:  |  Size: 335 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 7L4 12l5 5"></path><path d="M4 12h10a6 6 0 0 1 0 12h-1.5"></path></svg>

After

Width:  |  Height:  |  Size: 266 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4"></path><path d="M8 8l4-4 4 4"></path><path d="M5 15v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3"></path></svg>

After

Width:  |  Height:  |  Size: 304 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5l9 16.5H3z"></path><line x1="12" y1="10" x2="12" y2="14"></line><circle cx="12" cy="17" r="0.9" fill="currentColor" stroke="none"></circle></svg>

After

Width:  |  Height:  |  Size: 344 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"></line><line x1="18" y1="6" x2="6" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 275 B

BIN
design/assets/logo-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

1841
design/reference/support.js Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

106
design/tokens.css Normal file
View file

@ -0,0 +1,106 @@
/* ============================================================
Scholarr design tokens (FROZEN CONTRACT)
Single source of truth for color, type, space, radius,
elevation, and motion. Light is default; dark applies by OS
preference OR an explicit [data-theme="dark"] on a root
element. [data-theme="light"] forces light under a dark OS.
Colors authored in oklch; sRGB hex beside each is the
reference/fallback. See DESIGN.md §3 for the full contrast
proof table. Text colors are chosen so every pairing used
for text meets WCAG AA in both themes.
============================================================ */
:root {
/* Type ---------------------------------------------------- */
--font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace;
/* Type scale (px) ---------------------------------------- */
--fs-display: 27px; --fs-title: 17px; --fs-body: 14px;
--fs-sm: 12.5px; --fs-xs: 11px; --fs-micro: 10px;
--lh-tight: 1.25; --lh-body: 1.45;
/* Space scale (px) --------------------------------------- */
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
--sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
/* Radius ------------------------------------------------- */
--r-sm: 6px; --r-md: 10px; --r-lg: 16px; --r-pill: 20px;
/* Elevation ---------------------------------------------- */
--sh-1: 0 1px 2px oklch(0.26 0.02 62 / 0.06), 0 2px 8px oklch(0.26 0.02 62 / 0.05);
--sh-2: 0 2px 6px oklch(0.26 0.02 62 / 0.08), 0 12px 32px oklch(0.26 0.02 62 / 0.09);
/* Motion (see DESIGN.md §Motion). Frozen at 0 by reduced-motion below. */
--dur-fast: 120ms; --dur-base: 180ms; --dur-slow: 260ms;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* Breakpoint reference (custom props can't drive @media; documented here
and mirrored in DESIGN.md §Responsive): phone < 760px, wide >= 760px. */
/* Surfaces & ink (warm paper/ink) ------------------------ */
--paper: oklch(0.986 0.006 85); /* #fcfaf6 */
--surface: oklch(0.998 0.003 85); /* #fffefc */
--surface-2: oklch(0.965 0.008 84); /* #f6f3ee */
--ink: oklch(0.26 0.014 62); /* #29231d */
--ink-soft: oklch(0.46 0.012 62); /* #5d5751 */
--ink-faint: oklch(0.53 0.011 64); /* #76706b (AA on surface) */
--line: oklch(0.9 0.007 72); /* #e1ddd9 */
--line-strong: oklch(0.83 0.009 72); /* #cbc6c1 */
/* Accent (scholarly ink-blue) ---------------------------- */
--accent: oklch(0.5 0.1 248); /* #2e6799 */
--accent-soft: oklch(0.94 0.03 248); /* #dceeff */
--accent-ink: oklch(0.42 0.11 248); /* #055085 accent text on paper/soft */
--unread: oklch(0.5 0.1 248); /* #2e6799 */
--on-accent: oklch(0.99 0 0); /* #fcfcfc text ON solid fills */
/* Semantic confidence AND source health share one scale.
-soft = background, -ink = AA-safe text on that -soft. */
--hi: oklch(0.6 0.1 150); /* #519160 healthy / confident */
--hi-soft: oklch(0.94 0.04 150); /* #d9f3dd */
--hi-ink: oklch(0.44 0.11 150); /* #15632f */
--mid: oklch(0.66 0.12 74); /* #bd8630 cooling / likely */
--mid-soft: oklch(0.94 0.05 82); /* #fce9c6 */
--mid-ink: oklch(0.45 0.09 74); /* #734c0c */
--low: oklch(0.56 0.16 26); /* #c0453f failing / uncertain */
--low-soft: oklch(0.94 0.05 30); /* #ffe0d8 */
--low-ink: oklch(0.47 0.17 26); /* #a51e21 */
}
/* Dark tokens (shared by OS-preference and explicit opt-in) */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: oklch(0.205 0.004 255); --surface: oklch(0.247 0.005 255); --surface-2: oklch(0.295 0.006 255);
--ink: oklch(0.94 0.004 255); --ink-soft: oklch(0.76 0.005 255); --ink-faint: oklch(0.68 0.006 255);
--line: oklch(0.335 0.006 255); --line-strong: oklch(0.44 0.008 255);
--accent: oklch(0.72 0.11 248); --accent-soft: oklch(0.33 0.06 255); --accent-ink: oklch(0.83 0.09 248);
--unread: oklch(0.74 0.11 248); --on-accent: oklch(0.17 0.02 255);
--hi: oklch(0.74 0.12 152); --hi-soft: oklch(0.33 0.05 165); --hi-ink: oklch(0.85 0.11 152);
--mid: oklch(0.78 0.12 82); --mid-soft: oklch(0.35 0.05 85); --mid-ink: oklch(0.86 0.11 85);
--low: oklch(0.72 0.15 28); --low-soft: oklch(0.35 0.06 30); --low-ink: oklch(0.84 0.13 28);
--sh-1: 0 1px 2px oklch(0 0 0 / 0.3), 0 2px 8px oklch(0 0 0 / 0.28);
--sh-2: 0 2px 8px oklch(0 0 0 / 0.4), 0 16px 40px oklch(0 0 0 / 0.4);
}
}
:root[data-theme="dark"] {
--paper: oklch(0.205 0.004 255); --surface: oklch(0.247 0.005 255); --surface-2: oklch(0.295 0.006 255);
--ink: oklch(0.94 0.004 255); --ink-soft: oklch(0.76 0.005 255); --ink-faint: oklch(0.68 0.006 255);
--line: oklch(0.335 0.006 255); --line-strong: oklch(0.44 0.008 255);
--accent: oklch(0.72 0.11 248); --accent-soft: oklch(0.33 0.06 255); --accent-ink: oklch(0.83 0.09 248);
--unread: oklch(0.74 0.11 248); --on-accent: oklch(0.17 0.02 255);
--hi: oklch(0.74 0.12 152); --hi-soft: oklch(0.33 0.05 165); --hi-ink: oklch(0.85 0.11 152);
--mid: oklch(0.78 0.12 82); --mid-soft: oklch(0.35 0.05 85); --mid-ink: oklch(0.86 0.11 85);
--low: oklch(0.72 0.15 28); --low-soft: oklch(0.35 0.06 30); --low-ink: oklch(0.84 0.13 28);
--sh-1: 0 1px 2px oklch(0 0 0 / 0.3), 0 2px 8px oklch(0 0 0 / 0.28);
--sh-2: 0 2px 8px oklch(0 0 0 / 0.4), 0 16px 40px oklch(0 0 0 / 0.4);
}
/* Global accessibility defaults ---------------------------- */
*:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
}

11
docs/specs/README.md Normal file
View file

@ -0,0 +1,11 @@
# Architecture specs
Specs are frozen in the order listed in `TASKS.md`. Each spec must:
- distinguish locked decisions from open questions;
- define data and trust boundaries;
- state failure, recovery, migration, and observability behavior;
- identify deterministic fixtures and acceptance tests;
- receive explicit owner review before being marked frozen.
No implementation for a subsystem starts before its spec is frozen.