Bootstrap the Scholarr rewrite
375
design/DESIGN.md
Normal 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 | 15–17px | 700 | |
|
||||
| Publication title | serif | 14.5–16.5px | 700 unread / 500 read | `text-wrap: pretty` |
|
||||
| Body | sans | 13–14px | 400–500 | line-height 1.45 |
|
||||
| Meta / secondary | sans | 11.5–12.5px | 400 | `--ink-soft` |
|
||||
| Identifiers, timestamps, counts | mono | 10.5–11.5px | 400–700 | `--ink-faint` |
|
||||
| Uppercase micro-label | mono | 10px | 700 | letter-spacing 0.08–0.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 ≤ 820–900px, centered.
|
||||
|
||||
---
|
||||
|
||||
## 6. The two badge vocabularies
|
||||
|
||||
Both render as a pill: `--r-pill`, mono 10–11px 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
|
|
@ -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.
|
||||
BIN
design/assets/favicon-512.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
design/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
1
design/assets/icons/activity.svg
Normal 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 |
1
design/assets/icons/arrow-left.svg
Normal 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 |
1
design/assets/icons/authors.svg
Normal 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 |
1
design/assets/icons/book.svg
Normal 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 |
1
design/assets/icons/check.svg
Normal 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 |
1
design/assets/icons/chev-left.svg
Normal 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 |
1
design/assets/icons/chev-right.svg
Normal 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 |
1
design/assets/icons/circle.svg
Normal 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 |
1
design/assets/icons/dashboard.svg
Normal 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 |
1
design/assets/icons/dot.svg
Normal 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 |
1
design/assets/icons/external.svg
Normal 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 |
1
design/assets/icons/eye.svg
Normal 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 |
1
design/assets/icons/lock.svg
Normal 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 |
1
design/assets/icons/moon.svg
Normal 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 |
1
design/assets/icons/oa.svg
Normal 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 |
1
design/assets/icons/pending.svg
Normal 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 |
1
design/assets/icons/plus.svg
Normal 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 |
1
design/assets/icons/publications.svg
Normal 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 |
1
design/assets/icons/review.svg
Normal 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 |
1
design/assets/icons/search.svg
Normal 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 |
1
design/assets/icons/settings.svg
Normal 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 |
1
design/assets/icons/signout.svg
Normal 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 |
1
design/assets/icons/sources.svg
Normal 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 |
1
design/assets/icons/sun.svg
Normal 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 |
1
design/assets/icons/system.svg
Normal 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 |
1
design/assets/icons/undo.svg
Normal 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 |
1
design/assets/icons/upload.svg
Normal 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 |
1
design/assets/icons/warning.svg
Normal 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 |
1
design/assets/icons/x.svg
Normal 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
|
After Width: | Height: | Size: 133 KiB |
BIN
design/assets/logo-light.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
212
design/component-reference.html
Normal file
1078
design/reference/Scholarr.dc.html
Normal file
1841
design/reference/support.js
Normal file
BIN
design/reference/uploads/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
design/reference/uploads/logo-dark-tight.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
design/reference/uploads/logo-dark.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
design/reference/uploads/logo-light-tight.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
design/reference/uploads/scholar_logo.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
106
design/tokens.css
Normal 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; }
|
||||
}
|
||||