diff --git a/DESIGN.md b/DESIGN.md
index af281fa..180fbb8 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,46 +1,56 @@
---
name: jsPsych Metadata Generator
description: Guided Psych-DS metadata generator for jsPsych experiments
+# Palette is INHERITED from the jsPsych Docusaurus theme (@jspsych/docusaurus-theme,
+# css/jspsych.css). The theme is the single source of truth; these values are hardcoded
+# into the wizard's --c-* tokens because the iframe does not load the theme CSS. Keep in sync.
+# Role: green is the primary CTA color; orange is a <=10% accent (landing "open existing" icon).
colors:
- forest-deep: "#006738"
- forest-bright: "#13b24b"
- amber: "#f78f1e"
- amber-hover: "#e07d12"
- danger: "#d93025"
- ink: "#1a2e24"
- ink-2: "oklch(37% 0.012 155)"
+ forest-deep: "#00683e" # theme --ifm-color-primary
+ forest-bright: "#007447" # theme --ifm-color-primary-light (live/focus green, AA as text)
+ cta: "#00683e" # primary CTA fill (theme --ifm-color-primary)
+ cta-hover: "#006037" # theme --ifm-color-primary-dark
+ cta-text: "#ffffff" # label on the green fill (6.89:1)
+ amber: "#f18426" # theme --jspsych-orange (accent only, NOT primary CTA)
+ amber-hover: "#e3770e" # theme --jspsych-orange-dark
+ amber-text: "#b55800" # theme --jspsych-orange-ink (contrast-safe orange text)
+ danger: "#db3424" # theme --ifm-color-danger
+ ink: "#1f2622" # theme --ifm-color-content
+ ink-2: "#5e6561" # theme --ifm-color-content-secondary
ink-3: "oklch(52% 0.010 155)"
ink-4: "#9ab5a8"
- bg: "#f4f7f5"
+ bg: "#f4f8f5" # theme --ifm-background-surface-color (tinted page)
bg-sidebar: "oklch(94% 0.03 155)"
- bg-raised: "#ffffff"
- bg-input: "#ffffff"
+ bg-raised: "#fbfefc" # theme --ifm-background-color (near-white raised)
+ bg-input: "#fbfefc"
border: "#d6e0da"
border-sub: "#eaf0ed"
- dark-base: "#141918"
- dark-sidebar: "#0f1512"
- dark-raised: "#1d2420"
- dark-input: "#161d1a"
- dark-border: "#2c3830"
+ dark-cta: "#32bb64" # dark primary green (theme --ifm-color-primary, dark)
+ dark-cta-text: "#0d1310" # dark ink label on bright-green fill (7.54:1)
+ dark-base: "#151b17" # theme --ifm-background-surface-color (dark, page)
+ dark-sidebar: "#0d1310" # theme --ifm-background-color (dark, standalone-landing backdrop)
+ dark-raised: "#1b2620" # raised card (lighter than page)
+ dark-input: "#131a16"
+ dark-border: "#3a4641"
typography:
display:
- fontFamily: "'IBM Plex Sans', system-ui, sans-serif"
+ fontFamily: "'Lexend Variable', system-ui, -apple-system, sans-serif"
fontSize: "2.2rem"
fontWeight: 600
lineHeight: 1.2
headline:
- fontFamily: "'IBM Plex Sans', system-ui, sans-serif"
+ fontFamily: "'Lexend Variable', system-ui, -apple-system, sans-serif"
fontSize: "1.5rem"
fontWeight: 600
lineHeight: 1.3
color: "forest-deep (via --c-accent)"
body:
- fontFamily: "'IBM Plex Sans', system-ui, sans-serif"
+ fontFamily: "'Lexend Variable', system-ui, -apple-system, sans-serif"
fontSize: "0.9rem"
fontWeight: 400
lineHeight: 1.6
label:
- fontFamily: "'IBM Plex Sans', system-ui, sans-serif"
+ fontFamily: "'Lexend Variable', system-ui, -apple-system, sans-serif"
fontSize: "0.88rem"
fontWeight: 500
lineHeight: 1.4
@@ -60,17 +70,17 @@ spacing:
2xl: "48px"
components:
button-primary:
- backgroundColor: "{colors.amber}"
- textColor: "#ffffff"
+ backgroundColor: "{colors.cta}" # green (var --c-cta); dark: {colors.dark-cta}
+ textColor: "{colors.cta-text}" # white in light, dark ink (#0d1310) in dark mode
rounded: "{rounded.md}"
padding: "0.7em 2em"
fontSize: "1rem"
- note: "All forward-progress CTAs — Continue, Process, Import"
+ note: "All forward-progress CTAs — Continue, Process, Import, Validate"
button-primary-hover:
- backgroundColor: "{colors.amber-hover}"
+ backgroundColor: "{colors.cta-hover}" # theme primary-dark; dark: #20aa57
button-download:
- backgroundColor: "{colors.amber}"
- textColor: "#ffffff"
+ backgroundColor: "{colors.cta}"
+ textColor: "{colors.cta-text}"
rounded: "{rounded.md}"
padding: "0.7em 2em"
fontSize: "1rem"
@@ -95,11 +105,10 @@ components:
padding: "0.6rem 0.75rem"
input-focus:
borderBottomColor: "{colors.forest-bright}"
- boxShadow: "0 2px 8px rgba(19, 178, 75, 0.12)"
+ boxShadow: "var(--c-focus-glow)" # forest-bright glow: rgba(0,116,71,.16) light / rgba(50,187,100,.22) dark
sidebar-active:
- borderLeft: "3px solid var(--c-amber-text)"
- backgroundColor: "rgba(247, 143, 30, 0.12)"
- color: "var(--c-amber-text)"
+ backgroundColor: "var(--c-accent-wash)" # accent green @10% light / @14% dark — inset rounded pill
+ color: "var(--c-accent)"
fontWeight: 600
---
@@ -111,40 +120,48 @@ components:
This is a research-grade tool that knows what it is: an instrument for a specific job, used by people who have already decided to be here. It does not welcome visitors. It does not try to convert anyone. It opens, presents the task, and gets out of the way.
-The jsPsych logo is the color source of truth. It contains four colors in roughly equal visual weight: deep forest green (~50% of dots), warm amber/orange (~40%), lime-bright green (scattered), and coral red (small cluster). The UI expresses all four: forest-deep for structural authority (headings, borders, accent), amber for forward-progress actions, forest-bright for completion and focus states, and coral only for danger/destructive actions.
+The wizard's palette is **inherited from the jsPsych Docusaurus theme** (`@jspsych/docusaurus-theme`, `css/jspsych.css`) — the theme is the single source of truth for color. Because the embedded wizard iframe does not load the theme CSS, the theme's values are hardcoded into the wizard's `--c-*` tokens and **must be kept in sync** with it.
+
+The theme derives from the jsPsych logo but follows a clear hierarchy: **green is primary, orange is a ≤10% accent.** The UI expresses this as: green (theme primary) for structural authority (headings, borders) *and* for all forward-progress CTAs; forest-bright green for focus, completion and success states; orange only as a small accent (the landing "open existing project" icon); and danger red only for destructive actions.
**Key Characteristics:**
-- IBM Plex Sans — technical/academic character suited to a research tool
+- Lexend Variable — the shared jsPsych brand font, unifying the wizard with the docs site
+- Green is the primary CTA color throughout the wizard flow (matching the theme's `button--primary`)
- Forest-deep for structural elements; forest-bright for interactive/completion states (two distinct greens)
-- Amber as the primary CTA color throughout the wizard flow
+- Orange demoted to a single ≤10% accent: the landing "open existing project" icon
- Hybrid underline inputs: full border for field definition, thick bottom as the dominant edge, forest-bright focus glow
- Flat elevation: tonal layering, no shadows at rest
- Light-first; dark theme uses forest-tinted neutrals (not generic gray)
-- Sidebar active: amber left strip + amber wash (functional nav selection indicator, distinct from content-area side stripes)
## 2. Colors
-Every surface color carries a trace of forest green (`hue 155`) in its undertone, so the system reads as coherent across light and dark modes.
+Every value is inherited from the jsPsych Docusaurus theme (see the note at the top of this section) and every surface color carries a trace of forest green (`hue 155`) in its undertone, so the system reads as coherent across light and dark modes.
+
+> **Source of truth.** All colors below are hardcoded from `@jspsych/docusaurus-theme` (`css/jspsych.css`) into the wizard's `--c-*` tokens because the iframe does not load the theme CSS. When the theme's palette changes, update these tokens to match — the theme wins.
### Brand
-- **Forest Deep** (`#006738`): Structural authority. Step headings, sidebar borders, focus ring color (base), interactive link color. Token: `--c-forest-deep`. In dark mode this fails as text — use `--c-accent` (adaptive) which maps to forest-bright on dark.
-- **Forest Bright** (`#13b24b`): Interactive and completion. Input focus borders, focus glow, completed step checkmarks and labels, success states. Token: `--c-forest-bright`. The "lighter" of the two logo greens — expresses liveness.
-- **Amber** (`#f78f1e`, hover `#e07d12`): Forward-progress actions. All Continue/Process/Import/Download buttons. Active sidebar step indicator strip and wash. Token: `--c-amber`, `--c-amber-hover`. In light mode, text-on-light-bg amber uses `--c-amber-text` (`oklch(46% 0.17 55)`) for contrast.
-- **Danger** (`#d93025`): Destructive and irreversible only. Start Over, error text, required-field asterisks. Token: `--c-danger`.
+- **Primary Green / Forest Deep** (`#00683e`, theme `--ifm-color-primary`): Structural authority **and** the primary CTA fill. Sidebar borders, focus ring color (base), interactive link color; the fill of every forward-progress button (via `--c-cta`, hover `--c-cta-hover` `#006037`). Token: `--c-forest-deep` / `--c-cta`. In dark mode deep green fails as text — use `--c-accent` (adaptive) which maps to the dark primary green `#32bb64`.
+- **Forest Bright** (`#007447`, theme `--ifm-color-primary-light`; dark `#32bb64`): Interactive and completion. Input focus borders, focus glow, completed step checkmarks and labels, "live" states. Token: `--c-forest-bright`. The lighter/livelier of the two greens; chosen to stay AA as text on the pale sidebar.
+- **Orange / Amber** (`#f18426`, theme `--jspsych-orange`; dark `#ff9c3b`): **Accent only, ≤10% of the UI — no longer a button color.** Its sole rendered use is the landing "open existing project" icon, via `--c-amber-text` (`#b55800`, theme `--jspsych-orange-ink`, contrast-safe orange text). The `--c-amber`/`--c-amber-hover`/`--c-amber-wash` tokens remain defined as a documented accent scale but are not applied to any rendered element.
+- **Danger** — two-tier, both from the theme:
+ - **Danger base** (`#db3424`, theme `--ifm-color-danger`; dark `#f0563f`): fills, borders, required-field asterisks, and destructive-button labels (Start Over, Remove, confirm buttons). Token: `--c-danger`.
+ - **Danger text** (`#b3160a`, theme `--ifm-color-danger-contrast-foreground`; dark `#ff9d8c`): error/validation **message copy** only. Token: `--c-danger-text`. The base red is ~4.3:1 as small text on the light page (below AA), so message copy uses the contrast-foreground, which clears AA (≈6.1:1 light / ≈5.9:1 dark). Do not use the base red for running error text.
### The Adaptive `--c-accent` Token
-`--c-accent` resolves to `forest-deep` in light mode and `forest-bright` in dark mode. Use it wherever a green accent needs to work in both themes (headings, borders, focus rings) — never hardcode `#006738` in a context where it might render on a dark background.
+`--c-accent` resolves to `forest-deep` (`#00683e`) in light mode and the dark primary green (`#32bb64`) in dark mode. Use it wherever a green accent needs to work in both themes (headings, borders, focus rings) — never hardcode `#00683e` in a context where it might render on a dark background.
### Neutral (Light Theme)
-- **`--c-bg`** (`#f4f7f5`): Main content area background. Slightly green-tinted off-white — not pure white, not cream.
-- **`--c-bg-sidebar`** (`oklch(94% 0.03 155)`): Sidebar surface. Visibly greener and slightly darker than `--c-bg`, creating a VS Code-style tonal distinction between navigation and content.
-- **`--c-bg-raised`** (`#ffffff`): Elevated surfaces — cards, inputs, dialogs, list panels. Pure white sits visibly above the tinted page backgrounds.
-- **`--c-bg-input`** (`#ffffff`): Input field backgrounds. Same as raised.
-- **`--c-ink`** (`#1a2e24`): Primary text. Near-black with forest DNA.
-- **`--c-ink-2`** (`oklch(37% 0.012 155)`): Secondary text. Form descriptions, card subtitles.
+Elevation runs **page (tinted) < raised (near-white)** — cards/inputs float above the tinted page. Note the theme's names are inverted vs this intent: the theme's near-white `--ifm-background-color` becomes the wizard's *raised* tone, and the theme's tinted `--ifm-background-surface-color` becomes the wizard's *page* tone.
+
+- **`--c-bg`** (`#f4f8f5`, theme `--ifm-background-surface-color`): Main content area background. Slightly green-tinted off-white — the tinted page tone.
+- **`--c-bg-sidebar`** (`oklch(94% 0.03 155)`): Backdrop for the standalone Landing screen only. A slightly greener, deeper tint than `--c-bg`. (The embedded app sidebar no longer uses this token — it shares the content surface `--c-bg`.)
+- **`--c-bg-raised`** (`#fbfefc`, theme `--ifm-background-color`): Elevated surfaces — cards, inputs, dialogs, list panels. The near-white tone sits visibly above the tinted page.
+- **`--c-bg-input`** (`#fbfefc`): Input field backgrounds. Same as raised.
+- **`--c-ink`** (`#1f2622`, theme `--ifm-color-content`): Primary text. Near-black with forest DNA.
+- **`--c-ink-2`** (`#5e6561`, theme `--ifm-color-content-secondary`): Secondary text. Form descriptions, card subtitles.
- **`--c-ink-3`** (`oklch(52% 0.010 155)`): Hint text, placeholder text, secondary labels.
- **`--c-ink-4`** (`#9ab5a8`): Disabled and locked states only. Do not use for meaningful text.
- **`--c-border`** (`#d6e0da`): Input borders, card borders, section dividers.
@@ -152,45 +169,48 @@ Every surface color carries a trace of forest green (`hue 155`) in its undertone
### Neutral (Dark Theme)
-All dark surfaces carry forest-green DNA (`hue 155`) — never pure gray.
+All dark surfaces carry forest-green DNA (`hue 155`) — never pure gray. Elevation is preserved (**sidebar deepest < page < raised**); in dark mode the theme names read normally, so `--ifm-background-color` is the deepest tone and `--ifm-background-surface-color` is lighter.
-- **`--c-bg`** (`#141918`): Body background.
-- **`--c-bg-sidebar`** (`#0f1512`): Sidebar, slightly deeper than content.
-- **`--c-bg-raised`** (`#1d2420`): Cards, dialogs.
-- **`--c-bg-input`** (`#161d1a`): Input backgrounds.
-- **`--c-border`** (`#2c3830`): Borders on dark surfaces.
-- In dark mode: `--c-accent` = `#13b24b` (forest-bright), `--c-amber-text` = `#f78f1e`.
+- **`--c-bg`** (`#151b17`, theme `--ifm-background-surface-color`): Body/page background (mid tone).
+- **`--c-bg-sidebar`** (`#0d1310`, theme `--ifm-background-color`): Backdrop for the standalone Landing screen only (the embedded app sidebar shares `--c-bg`).
+- **`--c-bg-raised`** (`#1b2620`): Cards, dialogs — a touch lighter than the page so they still read as elevated.
+- **`--c-bg-input`** (`#131a16`): Input backgrounds (recessed).
+- **`--c-border`** (`#3a4641`): Borders on dark surfaces.
+- In dark mode: `--c-accent` = `#32bb64` (dark primary green), `--c-amber-text` = `#ffb866`, primary CTA text = dark ink `#0d1310`.
### The Two-Green Rule
-Forest-deep and forest-bright serve distinct roles:
+Forest-deep and forest-bright serve distinct roles. Both come from the theme's green ramp:
-- **Forest-deep** = structural authority: headings, primary borders, button outlines, sidebar surface tint
-- **Forest-bright** = interactive liveness: focus rings, input focus glow, completed step labels, checkmarks, success indicators
+- **Forest-deep** (`#00683e`) = structural authority **and** primary CTA fill: primary borders, sidebar surface tint, and every forward-progress button.
+- **Forest-bright** (`#007447` light / `#32bb64` dark) = interactive liveness: focus rings, input focus glow, completed step labels, checkmarks, success indicators.
-Never use forest-bright for structural/static elements. Never use forest-deep for focus rings or live states.
+Never use forest-bright for structural/static elements. Never use forest-deep for focus rings or live states. (In dark mode the two greens converge on `#32bb64`.)
### Named Rules
-**The Amber CTA Rule.** Amber (`#f78f1e`) is the primary forward-progress button color throughout the wizard. Every "Continue →", "Process", "Import", and "Download" button uses amber. This is intentional — amber comprises ~40% of the jsPsych logo and should have proportionate presence. Amber also appears on the active sidebar step (left strip + wash) and the "Open existing project" card icon on the landing page.
+**The Green CTA Rule.** The primary forward-progress button color is **green** (the theme primary, `--c-cta` → `#00683e` light / `#32bb64` dark), matching the docs site's `button--primary`. Every "Continue →", "Process", "Import", "Validate", and "Download" button uses it. In dark mode the label is dark ink (`#0d1310`), not white, because white on bright green fails WCAG. This is deliberate: green is the theme's primary, and green CTAs suit "The Quiet Instrument" — forward motion without shouting.
+
+**The Orange Accent Rule.** Orange (`--jspsych-orange`, `#f18426` / `#ff9c3b`) is a **minor accent capped at ≤10% of the UI** — never a button fill. Its one rendered use is the landing "open existing project" icon, tinted with `--c-amber-text` (`--jspsych-orange-ink`) for contrast. Do not reintroduce orange as a CTA color.
**The Forest DNA Rule.** Every surface color carries a trace of `hue 155` (forest green). Surface neutrals are tinted green, not warm (no beige/cream/sand). New surface colors must add 0.005–0.015 chroma toward `hue 155`.
## 3. Typography
-**Font:** IBM Plex Sans (Google Fonts, weights 400 / 500 / 600)
+**Font:** Lexend Variable (the shared jsPsych brand font)
-IBM Plex Sans was designed for scientific and technical documentation — it has more character than Inter while remaining highly legible in dense form-heavy interfaces. The monospace companion (IBM Plex Mono) is available but the current implementation uses Fira Code / Cascadia Code for code elements.
+Lexend Variable is the brand typeface of the shared jsPsych Docusaurus theme; using it in the wizard unifies typography across the docs site and the embedded wizard. It is a humanist sans that stays highly legible in dense, form-heavy interfaces. The monospace companion is Fira Code / Cascadia Code for code elements.
-Loaded via:
-```html
-
+Loaded via the same `@fontsource-variable/lexend` package the theme uses, so the CSS family name (`'Lexend Variable'`) and rendering match exactly and it works offline:
+```ts
+// packages/frontend/src/main.tsx
+import '@fontsource-variable/lexend';
```
### Hierarchy
- **Display** (600, `2.2rem`, lh 1.2): Landing page title. One instance.
-- **Headline** (600, `1.5rem`, lh 1.3, color: `var(--c-accent)`): Step page headings — colored forest-deep in light, forest-bright in dark. One per view.
+- **Headline** (600, `1.5rem`, lh 1.3, color: `var(--c-accent)`): Prominent heading scale. Note: step views no longer render a *visible* page title — the sidebar's highlighted active step supplies that context. Each view keeps a visually-hidden `
` (its step name) so heading structure and screen readers are preserved; the `.srOnly` clip utility lives in `index.css`.
- **Title** (600, `1rem`, lh 1.4): Card headings, section sub-headings.
- **Body** (400, `0.9rem`, lh 1.6): Form descriptions, field hints, prose. Cap at 65ch.
- **Label** (500, `0.88rem`, lh 1.4): Field labels, sidebar step names.
@@ -198,7 +218,7 @@ Loaded via:
### Named Rules
-**The One Family Rule.** IBM Plex Sans only for UI shell. Code blocks and monospace contexts use Fira Code / Cascadia Code as fallback chain.
+**The One Family Rule.** Lexend Variable only for UI shell. Code blocks and monospace contexts use Fira Code / Cascadia Code as fallback chain.
**The No Uppercase Rule.** No `text-transform: uppercase` anywhere. Hierarchy comes from weight and scale, not case.
@@ -207,14 +227,20 @@ Loaded via:
Flat by default. Depth is conveyed through tonal surface hierarchy (sidebar → content → raised → input) and borders. The hierarchy from darkest to lightest in light mode:
```
-bg-sidebar (oklch 94%) ← slightly deeper/greener
-bg (f4f7f5) ← main content area
-bg-raised / bg-input (#ffffff) ← white elevated surfaces
+bg-sidebar (oklch 94%) ← slightly deeper/greener
+bg (#f4f8f5) ← main content area (tinted page)
+bg-raised / bg-input (#fbfefc) ← near-white elevated surfaces
```
-Inputs and cards appear to "float" above the tinted content area because they are pure white on a tinted background.
+Inputs and cards appear to "float" above the tinted content area because they are near-white on a tinted background. (Dark mode preserves the same order: sidebar `#0d1310` deepest → page `#151b17` → raised `#1b2620`.)
+
+**Shadows:** Reserved for modal backdrops only (`rgba(0,0,0,0.5)` scrim). No drop shadows on cards, inputs, or buttons at rest. The input focus glow (`--c-focus-glow`, a forest-bright green glow) is the only shadow-like effect in the system — it's directional (below the field only) and communicates interactivity, not elevation.
+
+## 4b. Focus & Motion
-**Shadows:** Reserved for modal backdrops only (`rgba(0,0,0,0.5)` scrim). No drop shadows on cards, inputs, or buttons at rest. The input focus glow (`0 2px 8px rgba(19,178,75,0.12)`) is the only shadow-like effect in the system — it's directional (below the field only) and communicates interactivity, not elevation.
+**Keyboard focus.** Text inputs, textareas, and selects show focus as the underline + directional glow described above (their `:focus` rule sets `outline: none` and replaces it). Every *other* interactive control — cards, sidebar steps, buttons, the preview pill, links — gets one shared branded ring, defined once globally in `index.css`: `:focus-visible { outline: 2px solid var(--c-forest-bright); outline-offset: 2px }`. Forest-bright is the system's focus/liveness green (the Two-Green Rule). `:focus-visible` keeps the ring off mouse clicks; the outline follows each element's own radius. The higher-specificity input `:focus` rule wins, so the ring never doubles up on fields.
+
+**Reduced motion.** A global `@media (prefers-reduced-motion: reduce)` block collapses all transitions and the preview drawer's slide-in to a near-instant, non-animated state change. Honor it — don't add motion that ignores the preference.
## 5. Components
@@ -222,7 +248,7 @@ Inputs and cards appear to "float" above the tinted content area because they ar
Matte, no shadow, no transform on hover. Motion is background color only.
-- **Primary (Amber):** `#f78f1e` background, white text, `0.7em 2em` padding, `1rem` font size, `7px` radius. Hover → `#e07d12`. Used for all forward-progress CTAs: Continue, Process, Import, Download. This is the most common button in the flow.
+- **Primary (Green):** `var(--c-cta)` (`#00683e` light / `#32bb64` dark) background, `var(--c-cta-text)` label (white light / dark-ink `#0d1310` dark), `0.7em 2em` padding, `1rem` font size, `7px` radius. Hover → `var(--c-cta-hover)`. Used for all forward-progress CTAs: Continue, Process, Import, Validate, Download. This is the most common button in the flow, and it matches the docs site's green `button--primary`.
- **Ghost:** Transparent background, `ink-3` text. Hover shows subtle background tint. Used for cancel, collapse toggles, secondary links.
- **Danger:** Transparent, `--c-danger` text, `1px solid var(--c-danger-border)`. Hover adds `--c-danger-bg` tint. "Start Over" only.
- **Add/Dashed:** `1px dashed var(--c-accent)`, transparent bg, `var(--c-accent)` text, `opacity: 0.7` at rest → `1.0` on hover. Used for "Add author".
@@ -232,31 +258,38 @@ Matte, no shadow, no transform on hover. Motion is background color only.
The input style is a hybrid underline: full border for field definition, thick bottom as the dominant edge.
- **Resting:** `border: 1px solid var(--c-border)` (all sides, defines the field shape), `border-bottom: 2px solid var(--c-border)` (overrides bottom to 2px — the primary visual edge), `border-radius: 2px 2px 0 0`, `background: var(--c-bg-input)` (pure white, elevated above tinted page bg).
-- **Focus:** `border-bottom-color: var(--c-forest-bright)` (only the bottom activates — sides stay neutral), `box-shadow: 0 2px 8px rgba(19, 178, 75, 0.12)` (soft directional glow below the field). No outline. The forest-bright focus color is intentionally the brighter of the two greens — it signals liveness.
+- **Focus:** `border-bottom-color: var(--c-forest-bright)` (only the bottom activates — sides stay neutral), `box-shadow: var(--c-focus-glow)` (soft directional forest-bright glow below the field). No outline. The forest-bright focus color is intentionally the brighter of the two greens — it signals liveness.
- **Placeholder:** `var(--c-ink-3)` — sufficient contrast on white bg.
- **Disabled:** `var(--c-bg-raised)` background, `var(--c-ink-4)` text.
The `2px 2px 0 0` radius keeps corners mostly sharp (tool-like) while removing the harsh 90° edge.
+### Landing / Entry Screen
+
+The entry screen is **embed-aware** (via the `embedded` flag from `useTheme()`, mirroring the theme toggle and sidebar header):
+
+- **Standalone** (direct access, rare): the full hero — logo, `2.2rem` display title, description line, and the "What is Psych-DS?" collapsible — wrapped around the two-card choice. Sits on `var(--c-bg-sidebar)` (the deepest tint).
+- **Embedded** (the common case, iframed on the docs site): a lean start screen. The host page already supplies the brand, title, description, and a Psych-DS explainer, so all of that is dropped. Only a compact sentence-case heading ("Start a metadata file", `1.15rem`/600) precedes the two-card choice. Sits on `var(--c-bg)` — the app content background — so entering the wizard is a seamless tonal continuation rather than a jump from the deepest tint.
+
### Cards (Landing Page)
- **Corner Style:** `12px` radius
-- **Background:** `var(--c-bg-raised)` (`#ffffff`) — white on the green-tinted landing background
+- **Background:** `var(--c-bg-raised)` (`#fbfefc`) — near-white on the green-tinted landing background
- **Border:** `1px solid var(--c-border)`
- **Hover:** Border transitions to `var(--c-accent)`, background tints to `var(--c-bg-card-hover)`
- **Shadow:** None
### Sidebar Navigation
-The sidebar surface (`--c-bg-sidebar`) is visibly greener than the content area (`--c-bg`), creating a clear zone distinction like a VS Code-style navigation rail.
+The sidebar shares the content surface (`--c-bg`) — there is no separate zone. A `--c-border-sub` hairline divides it from the content on the right edge, and a `--c-border-sub` hairline sits above the footer; the intent is for the nav to feel like part of the same surface, not a walled-off rail.
+
+The steps read as a **vertical flow**, not a list: each step is a 22px circular node on a **vertical connector line** that threads every node, with a `1rem` label (larger than the surrounding UI, to anchor the sidebar). The connector is filled `var(--c-forest-bright)` through completed steps (progress made) and `var(--c-border)` for the road ahead. Nodes are drawn via `.step:not(:last-child)::before`, sized `calc(100% + --step-gap)` so equal-height rows land the line exactly on the next node.
-- **Step default:** `var(--c-ink-2)` text, no background. `border-left: 3px solid transparent` (space reserved to prevent layout shift on active).
-- **Step hover:** `var(--c-ink)` text, `rgba(19, 178, 75, 0.08)` background (forest-bright based — brighter/livelier than forest-deep).
-- **Step active:** `3px solid var(--c-amber-text)` left strip + `rgba(247, 143, 30, 0.12)` amber wash + `var(--c-amber-text)` text + 600 weight. The left strip is intentional in the sidebar navigation context — it is a functional selection indicator, not a decorative content-area stripe. This is distinct from the content-area side-stripe prohibition.
-- **Step completed:** `var(--c-forest-bright)` text (the bright green, distinct from default ink) + forest-bright `✓` indicator. The two-green system is visible here: completed steps are brighter-green, step headings are deep-green.
-- **Step locked:** `var(--c-ink-4)` text, `cursor: not-allowed`. WCAG exception for inactive UI components applies.
-- **Dot indicator (default/active):** 5×5px circle. Default: `var(--c-ink-4)`. Active: `var(--c-amber-text)`.
-- **Checkmark indicator (completed):** `var(--c-forest-bright)`.
+- **Node — upcoming (default):** hollow ring — `2px solid var(--c-border)`, `var(--c-bg)` fill. Label `var(--c-ink-2)`, no background.
+- **Node — active:** `var(--c-accent)` ring with a filled `var(--c-accent)` centre dot (`◉`). Label `var(--c-accent)`, 600 weight, on a `var(--c-accent-wash)` rounded highlight (accent green at 10% light / 14% dark) that anchors "you are here."
+- **Node — completed:** solid `var(--c-forest-bright)` disc with a `✓` in `var(--c-cta-text)` (white in light, dark ink in dark — legible on the green in both modes). Label `var(--c-ink)` (full strength — done and still clickable to revisit).
+- **Node — locked:** hollow muted ring (same as upcoming); label `var(--c-ink-3)` (lifted to oklch 66% in light), `cursor: not-allowed`. WCAG exception for inactive UI components applies.
+- **Hover:** `var(--c-ink)` label + `var(--c-bg-raised)` background (subtle neutral lift; excluded from the active step).
### Content Layout
@@ -280,26 +313,30 @@ Content area padding: `3rem 2rem`. The minimum 2rem side padding ensures the con
### Do:
-- **Do** use amber (`#f78f1e`) for all primary forward-progress CTAs: Continue, Process, Import, Download. Amber is proportionate to its presence in the logo (~40%) and should feel present throughout the flow.
-- **Do** use `var(--c-accent)` (adaptive: forest-deep in light, forest-bright in dark) for step page headings, links, and structural green elements.
+- **Do** keep the wizard's `--c-*` tokens in sync with `@jspsych/docusaurus-theme` (`css/jspsych.css`) — the theme is the single source of truth for color.
+- **Do** use `var(--c-cta)` (green) for all primary forward-progress CTAs: Continue, Process, Import, Validate, Download. In dark mode pair it with `var(--c-cta-text)` (dark ink), never white.
+- **Do** use `var(--c-accent)` (adaptive: forest-deep in light, dark primary green in dark) for links and structural green elements.
- **Do** use `var(--c-forest-bright)` for focus states, active input borders, completed step indicators, and success states.
- **Do** use `var(--c-forest-deep)` for structural elements: heading color in light mode, primary borders, sidebar tint.
- **Do** tint all neutral surface colors toward forest green (`hue 155`) by 0.005–0.015 chroma. Never pure neutral gray.
- **Do** give inputs a full 1px border for field definition, with the bottom at 2px — the thick bottom is the underline character, the sides define the shape.
- **Do** use forest-bright for the input focus glow, not forest-deep. The brighter green signals liveness.
-- **Do** use the amber left-strip + wash for the active sidebar step. This is a functional navigation selection indicator — it is explicitly permitted in the sidebar context.
+- **Do** use the green rounded highlight (`--c-accent-wash` background + `--c-accent` text) for the active sidebar step — a functional navigation selection indicator rendered as an inset rounded pill, not a stripe.
- **Do** center step content with `margin: 0 auto` at the specified max-widths.
-- **Do** use IBM Plex Sans (Google Fonts, weights 400/500/600) as the sole UI font.
+- **Do** use Lexend Variable (the shared jsPsych brand font) as the sole UI font.
### Don't:
+- **Don't** use amber/orange as a button or primary-CTA color. Orange is a ≤10% accent (the landing "open existing" icon only); green is the primary. Do not reintroduce the old "Amber CTA" pattern.
+- **Don't** hardcode brand hexes (greens/oranges/reds) in component CSS. Reference the `--c-*` tokens, which mirror the theme.
- **Don't** use `#646cff` or any blue-purple. It is Vite's scaffold color, not a brand color.
-- **Don't** apply `border-left` or `border-right` greater than 1px as a colored accent stripe on **content-area** elements: cards, list items, callouts, alerts. Use background tints instead. (The sidebar nav left-strip is a deliberate exception — it's a selection indicator, not a decorative stripe.)
+- **Don't** apply `border-left` or `border-right` greater than 1px as a colored accent stripe on **content-area** elements: cards, list items, callouts, alerts. Use background tints instead.
- **Don't** apply `gradient text` (`background-clip: text` with a gradient). Single solid color only.
- **Don't** use `text-transform: uppercase` with `letter-spacing` on labels, nav items, or headings.
- **Don't** add box shadows to cards, inputs, or buttons at rest. The only shadow-like effect is the directional input focus glow.
- **Don't** use warm beige/sand/cream backgrounds. Surface neutrals must tint toward forest green, not warmth.
- **Don't** use pure neutral gray for any surface or text color. Every ink and surface value has a trace of `hue 155`.
- **Don't** use forest-bright for structural/static elements (borders, headings at rest). It is specifically the "live/interactive" green.
-- **Don't** use forest-deep as text on dark backgrounds — it fails contrast (~1.7:1 on dark surfaces). Use `var(--c-accent)` which adapts to forest-bright in dark mode.
+- **Don't** use forest-deep as text on dark backgrounds — it fails contrast (~1.7:1 on dark surfaces). Use `var(--c-accent)` which adapts to the dark primary green in dark mode.
+- **Don't** put white text on the dark-mode green CTA fill (`#32bb64`) — it fails WCAG (~2.5:1). Use dark ink (`var(--c-cta-text)` = `#0d1310`, ~7.5:1).
- **Don't** design to attract or impress. Every visual decision should reduce friction toward producing the JSON file.
diff --git a/package-lock.json b/package-lock.json
index e2653bf..e87ab38 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1923,6 +1923,15 @@
"node": ">=14"
}
},
+ "node_modules/@fontsource-variable/lexend": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/lexend/-/lexend-5.3.0.tgz",
+ "integrity": "sha512-3SXtiZ8rFbT0oaPzEboCG5RM3jtyhpNxandh1jWNLqvrJRfV/eP3R+GGw+o5e3hS1uqmAGutKxSvaWG5oyrimA==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@gulpjs/messages": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz",
@@ -14397,6 +14406,7 @@
"packages/frontend": {
"version": "0.0.2",
"dependencies": {
+ "@fontsource-variable/lexend": "^5.2.11",
"@jspsych/metadata": "^0.0.3",
"fflate": "^0.8.3",
"jsonld": "^8.3.2",
diff --git a/packages/frontend/index.html b/packages/frontend/index.html
index 185b70d..0112936 100644
--- a/packages/frontend/index.html
+++ b/packages/frontend/index.html
@@ -15,9 +15,6 @@
(t === 'dark' || (!t && matchMedia('(prefers-color-scheme: dark)').matches)) ? 'dark' : 'light';
})();
-
-
-
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 0157fd1..b6d12b4 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -12,6 +12,7 @@
"typecheck": "tsc -b"
},
"dependencies": {
+ "@fontsource-variable/lexend": "^5.2.11",
"@jspsych/metadata": "^0.0.3",
"fflate": "^0.8.3",
"jsonld": "^8.3.2",
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index d69b689..718394d 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -48,7 +48,7 @@ function App() {
)}
{page === 'landing'
- ?
+ ?
: = ({ title, right }) => (
-
-
{title}
- {right &&
{right}
}
-
-);
-
-export default PageHeader;
diff --git a/packages/frontend/src/components/Sidebar.module.css b/packages/frontend/src/components/Sidebar.module.css
index 98f73f9..c50a3fd 100644
--- a/packages/frontend/src/components/Sidebar.module.css
+++ b/packages/frontend/src/components/Sidebar.module.css
@@ -1,11 +1,11 @@
.sidebar {
width: 210px;
min-width: 210px;
- background-color: var(--c-bg-sidebar);
+ background-color: var(--c-bg);
display: flex;
flex-direction: column;
padding: 1.5rem 0 1rem;
- border-right: 1px solid var(--c-border);
+ border-right: 1px solid var(--c-border-sub);
}
.header {
@@ -30,46 +30,78 @@
.stepList {
list-style: none;
margin: 0;
- padding: 0;
+ /* Node column aligns with the header/footer text (1.25rem from the sidebar
+ edge): 0.5rem list padding + 0.75rem step padding lands the node there.
+ --step-gap feeds both the row gap and the connector height below. */
+ padding: 0 0.5rem;
+ --step-gap: 0.3rem;
display: flex;
flex-direction: column;
- gap: 1px;
+ gap: var(--step-gap);
flex: 1;
}
+/* Each step is a row: a node on a vertical connector, then a label. The
+ connector threads every node so the list reads as a flow of steps, not a
+ bullet list — filled green through completed steps, muted for the road ahead. */
.step {
+ position: relative;
display: flex;
align-items: center;
- gap: 0.65rem;
+ gap: 0.7rem;
width: 100%;
- padding: 0.55rem 1.25rem 0.55rem calc(1.25rem - 3px);
+ padding: 0.55rem 0.75rem;
background: none;
border: none;
- border-left: 3px solid transparent;
- border-radius: 0;
+ border-radius: 7px;
color: var(--c-ink-2);
- font-size: 0.88rem;
+ font-size: 1rem;
font-weight: 400;
font-family: inherit;
cursor: pointer;
text-align: left;
- transition: color 0.15s, background-color 0.15s, border-color 0.15s;
+ transition: color 0.15s, background-color 0.15s;
+}
+
+/* Connector: from this node's center down to the next node's. Equal-height rows
+ make calc(100% + gap) land exactly on the next node. Paints over the active
+ pill (positioned z-index:0 beats the button background) but under the node
+ (z-index:1). Every .step is the ONLY child of its
, so the last step is
+ suppressed via the last
— NOT :last-child on the button (always true).
+ Green through completed steps (progress), muted for the road ahead. */
+.step::before {
+ content: '';
+ position: absolute;
+ left: calc(0.75rem + 11px); /* step padding-left + node radius = node center */
+ top: 50%;
+ width: 2px;
+ height: calc(100% + var(--step-gap));
+ transform: translateX(-50%);
+ background: var(--c-border);
+ z-index: 0;
+}
+
+.stepList li:last-child .step::before {
+ display: none;
}
-.step:hover:not(:disabled) {
+.completed::before {
+ background: var(--c-forest-bright);
+}
+
+.step:hover:not(:disabled):not(.active) {
color: var(--c-ink);
- background-color: var(--c-bg-card-hover);
+ background-color: var(--c-bg-raised);
}
.active {
- border-left-color: var(--c-amber-text);
- background-color: rgba(247, 143, 30, 0.12);
- color: var(--c-amber-text);
+ background-color: var(--c-accent-wash);
+ color: var(--c-accent);
font-weight: 600;
}
.completed {
- color: var(--c-ink-2);
+ color: var(--c-ink);
}
.locked {
@@ -77,35 +109,53 @@
cursor: not-allowed;
}
+/* Node: a 22px circle on the connector. Default = hollow muted ring (upcoming);
+ active = accent ring with a filled center dot; completed = solid green disc
+ with a check. */
.indicator {
- width: 16px;
+ position: relative;
+ z-index: 1;
+ box-sizing: border-box;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 2px solid var(--c-border);
+ background: var(--c-bg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
- font-size: 0.75rem;
- color: var(--c-forest-bright);
+ font-size: 0.72rem;
+ font-weight: 600;
+ color: var(--c-cta-text); /* the ✓ glyph — legible on the green disc, both modes */
+ transition: border-color 0.15s, background-color 0.15s;
+}
+
+.active .indicator {
+ border-color: var(--c-accent);
}
+.completed .indicator {
+ border-color: var(--c-forest-bright);
+ background: var(--c-forest-bright);
+}
+
+/* Inner dot — only on the active node (ring + dot); hidden on the others. */
.dot {
- display: block;
- width: 5px;
- height: 5px;
+ display: none;
+ width: 8px;
+ height: 8px;
border-radius: 50%;
- background-color: var(--c-ink-4);
+ background-color: var(--c-accent);
}
.active .dot {
- background-color: var(--c-amber-text);
-}
-
-.locked .dot {
- background-color: var(--c-ink-3);
+ display: block;
}
.footer {
padding: 1rem 1.25rem 0;
- border-top: 1px solid var(--c-border);
+ border-top: 1px solid var(--c-border-sub);
margin-top: 0.5rem;
display: flex;
flex-direction: column;
@@ -144,15 +194,14 @@
border-color: var(--c-danger);
}
-/* Light mode: locked steps need to look clearly faded vs the dark base step color.
- --c-ink-3 (oklch 52%) is too close to --c-ink-2 (oklch 37%) on the pale sidebar. */
+/* Light mode: locked steps read clearly faded — --c-ink-3 (oklch 52%) sits too
+ close to --c-ink-2 (37%) on the pale sidebar, so lift the locked label lighter. */
:global([data-theme="light"]) .locked { color: oklch(66% 0.008 155); }
-:global([data-theme="light"]) .locked .dot { background-color: oklch(66% 0.008 155); }
/* Light mode: Start Over border (--c-danger-border at oklch 88%) is near-invisible on the
pale sidebar (oklch 94%). Give it a tinted background and a readable border. */
:global(:root:not([data-theme="dark"])) .startOver {
- background-color: rgba(217, 48, 37, 0.07);
+ background-color: rgba(219, 52, 36, 0.07);
border-color: oklch(82% 0.09 27);
}
@@ -161,10 +210,9 @@
background-color: var(--c-danger-bg);
}
-/* Dark mode: increase contrast of sidebar text and border */
-:global([data-theme="dark"]) .sidebar { border-right-color: #4e5f57; }
+/* Dark mode: lift sidebar text contrast (the divider stays a hairline so the
+ nav reads as part of the content surface, not a walled-off zone). */
:global([data-theme="dark"]) .appTitle { color: var(--c-ink); }
-:global([data-theme="dark"]) .step:not(.active):not(.locked) { color: oklch(82% 0.012 155); }
:global([data-theme="dark"]) .psychdsLink { color: var(--c-ink-2); }
:global([data-theme="dark"]) .psychdsLink:hover { color: var(--c-forest-bright); }
diff --git a/packages/frontend/src/index.css b/packages/frontend/src/index.css
index 67875a0..cc60063 100644
--- a/packages/frontend/src/index.css
+++ b/packages/frontend/src/index.css
@@ -8,24 +8,32 @@ html, body, #root {
height: 100%;
}
+/* -------------------------------------------------------------------------
+ COLOR IS INHERITED FROM THE jsPsych DOCUSAURUS THEME.
+ Every value below is hardcoded from @jspsych/docusaurus-theme (css/jspsych.css)
+ because the wizard iframe does not load the theme CSS. The theme is the single
+ source of truth — when it changes, update these values to match. Token NAMES
+ are kept stable so component CSS does not churn; only VALUES and ROLES change.
+ Role: green is the primary CTA color; orange is a <=10% accent (landing "open existing" icon).
+ ------------------------------------------------------------------------- */
:root {
- /* Brand */
- --c-forest-deep: #006738;
- --c-forest-bright: #13b24b;
- --c-amber: #f78f1e;
- --c-amber-hover: #e07d12;
- --c-danger: #d93025;
-
- /* Surfaces — light */
- --c-bg: #f4f7f5;
- --c-bg-sidebar: oklch(94% 0.03 155);
- --c-bg-raised: #ffffff;
- --c-bg-input: #ffffff;
+ /* Brand (theme green ramp + orange accent) */
+ --c-forest-deep: #00683e; /* theme --ifm-color-primary */
+ --c-forest-bright: #007447; /* theme --ifm-color-primary-light — "live" green, AA as text */
+ --c-amber: #f18426; /* theme --jspsych-orange (accent only, not primary CTA) */
+ --c-amber-hover: #e3770e; /* theme --jspsych-orange-dark */
+ --c-danger: #db3424; /* theme --ifm-color-danger */
+
+ /* Surfaces — light (elevation: page tinted < raised near-white) */
+ --c-bg: #f4f8f5; /* theme --ifm-background-surface-color (tinted page) */
+ --c-bg-sidebar: oklch(94% 0.03 155); /* a touch deeper/greener than the page */
+ --c-bg-raised: #fbfefc; /* theme --ifm-background-color (near-white raised) */
+ --c-bg-input: #fbfefc;
--c-bg-card-hover: oklch(97% 0.03 155);
- /* Ink — light (forest DNA via trace chroma, not saturated green) */
- --c-ink: #1a2e24;
- --c-ink-2: oklch(37% 0.012 155);
+ /* Ink — light (theme content ramp; forest DNA via trace chroma) */
+ --c-ink: #1f2622; /* theme --ifm-color-content */
+ --c-ink-2: #5e6561; /* theme --ifm-color-content-secondary */
--c-ink-3: oklch(52% 0.010 155);
--c-ink-4: #9ab5a8;
@@ -34,29 +42,41 @@ html, body, #root {
--c-border-sub: #eaf0ed;
/* Interactive accent — forest-deep in light, forest-bright in dark */
- --c-accent: #006738;
+ --c-accent: #00683e;
+ --c-accent-wash: rgba(0, 104, 62, 0.10); /* accent @ 10% — active-nav highlight */
- /* Amber for text/icons — darker in light mode to hit contrast on light surfaces */
- --c-amber-text: oklch(46% 0.17 55);
+ /* Primary CTA (green) — fill / hover / label */
+ --c-cta: #00683e; /* theme --ifm-color-primary */
+ --c-cta-hover: #006037; /* theme --ifm-color-primary-dark */
+ --c-cta-text: #ffffff; /* white on fill = 6.89:1 (AA) */
+
+ /* Input focus glow — forest-bright green, directional (below the field) */
+ --c-focus-glow: 0 2px 8px rgba(0, 116, 71, 0.16);
+
+ /* Orange accent (landing "open existing" icon only) */
+ --c-amber-text: #b55800; /* theme --jspsych-orange-ink — contrast-safe orange text */
+ --c-amber-wash: rgba(241, 132, 38, 0.12); /* theme orange @ 12% */
/* Type / status badge */
--c-badge-bg: oklch(94% 0.04 155);
- --c-badge-text: #006738;
+ --c-badge-text: #00683e;
--c-badge-border: oklch(87% 0.07 155);
- /* Semantic — success */
- --c-success-text: #13b24b;
- --c-success-bg: oklch(95% 0.04 145);
+ /* Semantic — success (theme success contrast pair) */
+ --c-success-text: #00622d; /* theme --ifm-color-success-contrast-foreground */
+ --c-success-bg: #e6fae9; /* theme --ifm-color-success-contrast-background */
--c-success-border: oklch(85% 0.08 145);
- /* Semantic — warning callout */
- --c-warn-text: oklch(48% 0.14 60);
- --c-warn-bg: oklch(97% 0.025 70);
+ /* Semantic — warning callout (theme warning contrast pair) */
+ --c-warn-text: #8a4d00; /* theme --ifm-color-warning-contrast-foreground */
+ --c-warn-bg: #fff1e3; /* theme --ifm-color-warning-contrast-background */
--c-warn-border: oklch(86% 0.07 70);
- --c-warn-icon: oklch(62% 0.17 60);
+ --c-warn-icon: #f18426; /* theme --jspsych-orange */
- /* Semantic — danger */
- --c-danger-bg: oklch(97% 0.02 27);
+ /* Semantic — danger. Two-tier: base for fills/borders/asterisks/destructive buttons,
+ -text (contrast-foreground) for error MESSAGE copy so it clears AA. */
+ --c-danger-text: #b3160a; /* theme --ifm-color-danger-contrast-foreground */
+ --c-danger-bg: #ffede8; /* theme --ifm-color-danger-contrast-background */
--c-danger-border: oklch(88% 0.06 27);
/* Layout */
@@ -66,42 +86,58 @@ html, body, #root {
/* Dark tokens — applied when data-theme="dark" (set synchronously by the
inline script in index.html before first paint, so no flash). */
:root[data-theme="dark"] {
- --c-bg: #232b27;
- --c-bg-sidebar: #1c2520;
- --c-bg-raised: #2b3430;
- --c-bg-input: #212a25;
- --c-bg-card-hover: #2e3a35;
-
- --c-ink: oklch(92% 0.008 155);
- --c-ink-2: oklch(83% 0.012 155);
+ /* Surfaces — dark (elevation: sidebar deepest < page < raised). Theme names read
+ "normally" in dark: --ifm-background-color is the deepest tone, surface is lighter. */
+ --c-bg: #151b17; /* theme --ifm-background-surface-color (dark, tinted page) */
+ --c-bg-sidebar: #0d1310; /* theme --ifm-background-color (dark, deepest zone) */
+ --c-bg-raised: #1b2620; /* raised card — a touch lighter than the page */
+ --c-bg-input: #131a16; /* recessed input field */
+ --c-bg-card-hover: #202c25;
+
+ --c-ink: #e5e9e6; /* theme --ifm-color-content (dark) */
+ --c-ink-2: #a0a7a2; /* theme --ifm-color-content-secondary (dark) */
--c-ink-3: oklch(68% 0.010 155);
--c-ink-4: oklch(48% 0.01 155);
--c-border: #3a4641;
--c-border-sub: #2e3a34;
- --c-accent: #13b24b;
- --c-amber-text: #f78f1e;
- --c-danger: oklch(68% 0.19 27);
+ --c-forest-bright: #32bb64; /* theme --ifm-color-primary (dark) — "live" green */
+ --c-accent: #32bb64; /* theme --ifm-color-primary (dark) */
+ --c-accent-wash: rgba(50, 187, 100, 0.14); /* accent @ 14% — active-nav highlight */
+ --c-amber: #ff9c3b; /* theme --jspsych-orange (dark) */
+ --c-amber-hover: #f18426; /* theme --jspsych-orange-dark (dark) */
+ --c-amber-text: #ffb866; /* theme --jspsych-orange-ink (dark) */
+ --c-amber-wash: rgba(255, 156, 59, 0.15); /* theme orange (dark) @ 15% */
+ --c-danger: #f0563f; /* theme --ifm-color-danger (dark) */
+
+ /* Primary CTA (green). Bright green + white fails WCAG, so use dark ink on the fill. */
+ --c-cta: #32bb64; /* theme --ifm-color-primary (dark) */
+ --c-cta-hover: #20aa57; /* theme --ifm-color-primary-dark (dark) */
+ --c-cta-text: #0d1310; /* dark ink on fill = 7.54:1 (AA) */
- --c-badge-bg: rgba(0, 103, 56, 0.18);
- --c-badge-text: #13b24b;
- --c-badge-border: rgba(0, 103, 56, 0.35);
+ --c-focus-glow: 0 2px 8px rgba(50, 187, 100, 0.22);
- --c-success-bg: rgba(19, 178, 75, 0.10);
- --c-success-border: rgba(19, 178, 75, 0.28);
+ --c-badge-bg: rgba(0, 104, 62, 0.18);
+ --c-badge-text: #32bb64;
+ --c-badge-border: rgba(0, 104, 62, 0.35);
- --c-warn-text: oklch(72% 0.14 60);
- --c-warn-bg: oklch(22% 0.04 70);
+ --c-success-text: #74e29a; /* theme --ifm-color-success-contrast-foreground (dark) */
+ --c-success-bg: #0e2a1a; /* theme --ifm-color-success-contrast-background (dark) */
+ --c-success-border: rgba(43, 191, 106, 0.28);
+
+ --c-warn-text: #ffc285; /* theme --ifm-color-warning-contrast-foreground (dark) */
+ --c-warn-bg: #2c1d0c; /* theme --ifm-color-warning-contrast-background (dark) */
--c-warn-border: oklch(38% 0.08 70);
- --c-warn-icon: #f78f1e;
+ --c-warn-icon: #ff9c3b;
- --c-danger-bg: rgba(217, 48, 37, 0.12);
- --c-danger-border: rgba(217, 48, 37, 0.38);
+ --c-danger-text: #ff9d8c; /* theme --ifm-color-danger-contrast-foreground (dark) */
+ --c-danger-bg: #2e1512; /* theme --ifm-color-danger-contrast-background (dark) */
+ --c-danger-border: rgba(240, 86, 63, 0.38);
}
body {
- font-family: 'IBM Plex Sans', system-ui, Helvetica, Arial, sans-serif;
+ font-family: 'Lexend Variable', system-ui, -apple-system, sans-serif;
line-height: 1.5;
font-weight: 400;
background-color: var(--c-bg);
@@ -127,7 +163,7 @@ a:hover {
border-radius: 20px;
padding: 0.35rem 0.85rem;
font-size: 0.82rem;
- font-family: 'IBM Plex Sans', system-ui, Helvetica, Arial, sans-serif;
+ font-family: 'Lexend Variable', system-ui, -apple-system, sans-serif;
color: var(--c-ink-2);
cursor: pointer;
display: inline-flex;
@@ -151,6 +187,21 @@ a:hover {
border-color: #607870;
}
+/* Visually-hidden utility — removes an element from the visual layout while
+ keeping it available to screen readers and the accessibility/heading tree.
+ Used for each step's page heading now that the visible title bar is gone. */
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
* {
scrollbar-width: thin;
scrollbar-color: var(--c-ink-4) transparent;
@@ -173,3 +224,27 @@ a:hover {
::-webkit-scrollbar-thumb:hover {
background-color: var(--c-ink-3);
}
+
+/* Keyboard focus ring — one branded indicator for every interactive control
+ that doesn't define its own. Inputs/textareas/selects keep their underline +
+ focus-glow treatment: their `.field:focus` rule (higher specificity) already
+ sets `outline: none`, so this ring never doubles up on them. `:focus-visible`
+ keeps the ring off mouse clicks. Forest-bright is the system's focus/liveness
+ green (the two-green rule); the outline follows each element's own radius. */
+:focus-visible {
+ outline: 2px solid var(--c-forest-bright);
+ outline-offset: 2px;
+}
+
+/* Respect users who ask for less motion: collapse transitions and the preview
+ drawer's slide-in to a near-instant, non-animated state change. */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx
index 9ab5175..f4f96f2 100644
--- a/packages/frontend/src/main.tsx
+++ b/packages/frontend/src/main.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { sweepStaleStagingDirs } from './staging/stagedFileStore'
+import '@fontsource-variable/lexend'
import './index.css'
// Reclaim OPFS staging subdirs from previous sessions that closed or crashed before clearing.
diff --git a/packages/frontend/src/pages/Authors.module.css b/packages/frontend/src/pages/Authors.module.css
index b00ed55..9692cd1 100644
--- a/packages/frontend/src/pages/Authors.module.css
+++ b/packages/frontend/src/pages/Authors.module.css
@@ -5,15 +5,6 @@
padding-top: 1rem;
}
-.count {
- font-size: 0.9rem;
- font-weight: 400;
- color: var(--c-ink-2);
- background: var(--c-border-sub);
- border-radius: 10px;
- padding: 0.1em 0.55em;
-}
-
.subtext {
margin: 0 0 1.5rem;
font-size: 0.86rem;
@@ -116,7 +107,7 @@
.input:focus {
outline: none;
border-bottom-color: var(--c-forest-bright);
- box-shadow: 0 2px 8px rgba(19, 178, 75, 0.12);
+ box-shadow: var(--c-focus-glow);
}
@@ -139,7 +130,7 @@
.orcidGroup:focus-within {
border-bottom-color: var(--c-forest-bright);
- box-shadow: 0 2px 8px rgba(19, 178, 75, 0.12);
+ box-shadow: var(--c-focus-glow);
}
.orcidPrefix {
@@ -355,7 +346,7 @@
.bulkTextarea:focus {
outline: none;
border-bottom-color: var(--c-forest-bright);
- box-shadow: 0 2px 8px rgba(19, 178, 75, 0.12);
+ box-shadow: var(--c-focus-glow);
}
.bulkTextarea::placeholder {
@@ -368,8 +359,8 @@
}
.importBtn {
- background-color: var(--c-amber);
- color: #ffffff;
+ background-color: var(--c-cta);
+ color: var(--c-cta-text);
border: none;
border-radius: 6px;
padding: 0.45em 1.2em;
@@ -381,7 +372,7 @@
}
.importBtn:hover {
- background-color: var(--c-amber-hover);
+ background-color: var(--c-cta-hover);
}
.cancelBtn {
@@ -404,8 +395,8 @@
/* Continue */
.continueBtn {
- background-color: var(--c-amber);
- color: #ffffff;
+ background-color: var(--c-cta);
+ color: var(--c-cta-text);
border: none;
border-radius: 7px;
padding: 0.6em 1.6em;
@@ -417,5 +408,5 @@
}
.continueBtn:hover {
- background-color: var(--c-amber-hover);
+ background-color: var(--c-cta-hover);
}
diff --git a/packages/frontend/src/pages/Authors.tsx b/packages/frontend/src/pages/Authors.tsx
index 3d5d61c..19cd2bb 100644
--- a/packages/frontend/src/pages/Authors.tsx
+++ b/packages/frontend/src/pages/Authors.tsx
@@ -1,6 +1,5 @@
import { useState, useRef } from 'react';
import JsPsychMetadata, { AuthorFields } from '@jspsych/metadata';
-import PageHeader from '../components/PageHeader';
import styles from './Authors.module.css';
interface AuthorsProps {
@@ -187,11 +186,9 @@ const Authors: React.FC = ({ jsPsychMetadata, onComplete }) => {
}
};
- const savedCount = rows.filter(r => r.committedName).length;
-
return (
<>
- Authors {savedCount}>} />
+
Select your data folder or upload a .zip archive. CSV and JSON files will be processed; other file types are skipped.
diff --git a/packages/frontend/src/pages/Landing.module.css b/packages/frontend/src/pages/Landing.module.css
index 36b18a0..b3d2a28 100644
--- a/packages/frontend/src/pages/Landing.module.css
+++ b/packages/frontend/src/pages/Landing.module.css
@@ -8,6 +8,28 @@
background-color: var(--c-bg-sidebar);
}
+/* Embedded on the docs site: the host already frames the tool, so we drop the
+ hero and sit on the app content background (--c-bg) for a seamless tonal
+ continuation into the wizard — no jump from the deepest tint on card click. */
+.embedded {
+ background-color: var(--c-bg);
+}
+
+/* Compact, calm entry heading shown only in the embedded lean screen. */
+.leanHeading {
+ font-size: 1.15rem;
+ font-weight: 600;
+ color: var(--c-ink);
+ margin: 0 0 1.75rem;
+ text-align: center;
+}
+
+/* With the hero gone, drop the trailing card margin so the two cards + heading
+ stay optically centered rather than pushed upward. */
+.embedded .cards {
+ margin-bottom: 0;
+}
+
.header {
text-align: center;
margin-bottom: 1.25rem;
diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx
index 5ec971a..07add75 100644
--- a/packages/frontend/src/pages/Landing.tsx
+++ b/packages/frontend/src/pages/Landing.tsx
@@ -4,9 +4,15 @@ import logo from '../assets/jspsych-logo-no-text.svg';
interface LandingProps {
onStart: (isNew: boolean, file?: File) => void;
+ /**
+ * True when the wizard runs inside the docs-site iframe. The surrounding page
+ * already supplies the brand, title, description, and a Psych-DS explainer, so
+ * the embedded entry drops the standalone hero for a lean start screen.
+ */
+ embedded?: boolean;
}
-const Landing: React.FC = ({ onStart }) => {
+const Landing: React.FC = ({ onStart, embedded = false }) => {
const fileInputRef = useRef(null);
const [psychDsOpen, setPsychDsOpen] = useState(false);
@@ -16,40 +22,46 @@ const Landing: React.FC = ({ onStart }) => {
};
return (
-
-
-
-
jsPsych Metadata Generator
-
- Generate Psych-DS compliant metadata for your jsPsych experiments.
-
-
+
+ {embedded ? (
+
Start a metadata file
+ ) : (
+ <>
+
+
+
jsPsych Metadata Generator
+
+ Generate Psych-DS compliant metadata for your jsPsych experiments.
+
+
-
-
- {psychDsOpen && (
-
- Psych-DS is an open standard for organizing and documenting psychological
- datasets. It defines a consistent folder structure and a metadata file
- (dataset_description.json) that makes your experiment data
- easier to share, archive, and reuse.{' '}
-
+
-
- )}
-
+ What is Psych-DS?
+ {psychDsOpen ? '▲' : '▼'}
+
+ {psychDsOpen && (
+
+ Psych-DS is an open standard for organizing and documenting psychological
+ datasets. It defines a consistent folder structure and a metadata file
+ (dataset_description.json) that makes your experiment data
+ easier to share, archive, and reuse.{' '}
+
+ Learn more →
+
+