diff --git a/e2e/accessibility.spec.ts b/e2e/accessibility.spec.ts index e1b1d67..e2157c9 100644 --- a/e2e/accessibility.spec.ts +++ b/e2e/accessibility.spec.ts @@ -20,24 +20,31 @@ async function violations(page: Page) { return results.violations.map((v) => `${v.id} (${v.impact}): ${v.nodes.map((n) => n.target.join(' ')).slice(0, 5).join(' | ')}`); } -test('every page passes the automated WCAG 2.1 AA checks', async ({ page }) => { - test.setTimeout(600_000); - const found: string[] = []; - for (const path of PAGES) { - await page.goto(path); - await expect(page.locator('main h1').first()).toBeVisible(); - for (const v of await violations(page)) found.push(`${path}: ${v}`); - } - expect(found, found.join('\n')).toEqual([]); -}); +// Both themes: the dark one has its own colours, so its own contrast to get right. +for (const colorScheme of ['light', 'dark'] as const) { + test.describe(`${colorScheme} theme`, () => { + test.use({ colorScheme }); -test('answer feedback passes the automated WCAG 2.1 AA checks', async ({ page }) => { - await page.goto('./lesson/03-4'); - await expect(page.locator('main h1').first()).toBeVisible(); - // Answer every choice block so the right, wrong and coach bubbles are all on the page. - for (const block of await page.locator('.choice-block').all()) { - const option = block.getByRole('button').first(); - if (await option.isEnabled()) await option.click(); - } - expect(await violations(page)).toEqual([]); -}); + test('every page passes the automated WCAG 2.1 AA checks', async ({ page }) => { + test.setTimeout(600_000); + const found: string[] = []; + for (const path of PAGES) { + await page.goto(path); + await expect(page.locator('main h1').first()).toBeVisible(); + for (const v of await violations(page)) found.push(`${path}: ${v}`); + } + expect(found, found.join('\n')).toEqual([]); + }); + + test('answer feedback passes the automated WCAG 2.1 AA checks', async ({ page }) => { + await page.goto('./lesson/03-4'); + await expect(page.locator('main h1').first()).toBeVisible(); + // Answer every choice block so the right, wrong and coach bubbles are all on the page. + for (const block of await page.locator('.choice-block').all()) { + const option = block.getByRole('button').first(); + if (await option.isEnabled()) await option.click(); + } + expect(await violations(page)).toEqual([]); + }); + }); +} diff --git a/index.html b/index.html index e5afdfe..c827370 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,14 @@ +
diff --git a/src/App.css b/src/App.css index 726cfd0..9cb7c4c 100644 --- a/src/App.css +++ b/src/App.css @@ -20,6 +20,9 @@ --brand-2: #2563eb; --brand-soft: #eef0ff; --brand-ink: #3730a3; + --brand-line: #c7d2fe; + --brand-tint: #f5f7ff; + --on-brand: #fff; --gradient: linear-gradient(135deg, #6366f1 0%, #2563eb 55%, #0ea5e9 100%); --success: #16a34a; @@ -31,6 +34,20 @@ --teal: #0d9488; --teal-soft: #ccfbf1; + /* Text, border and pale-fill shades of each state colour. */ + --success-ink: #166534; + --success-line: #86efac; + --success-tint: #f0fdf4; + --warn-ink: #92400e; + --warn-line: #fcd34d; + --warn-tint: #fffbeb; + --danger-ink: #991b1b; + --danger-line: #fca5a5; + --danger-tint: #fef2f2; + --teal-ink: #0f766e; + --teal-line: #99f6e4; + --teal-tint: #f0fdfa; + --radius-sm: 8px; --radius: 12px; --radius-lg: 18px; @@ -43,6 +60,50 @@ --medium: 260ms; } +/* Dark theme. index.html sets data-theme before the first paint, from the + student's saved choice or else their system setting. Code output, R plots + and the simulations keep their own fixed colours: they are figures. */ +:root[data-theme="dark"] { + color-scheme: dark; + --bg: #0b1120; + --surface: #131a2a; + --surface-2: #1c2436; + --ink: #e7ebf3; + --ink-2: #c5ccda; + --muted: #98a3b8; + --line: #273149; + --line-strong: #36425e; + + --brand: #8b93f8; + --brand-2: #6cb0fb; + --brand-soft: #232a52; + --brand-ink: #c7d2fe; + --brand-line: #3e4787; + --brand-tint: #181e3d; + --on-brand: #0b1120; + + --success-soft: #0e2618; + --warn-soft: #33250a; + --danger-soft: #3a1515; + --teal-soft: #0e2a28; + --success-ink: #86efac; + --success-line: #1f6b3d; + --success-tint: #0c1f15; + --warn-ink: #fcd34d; + --warn-line: #7a5a12; + --warn-tint: #241b08; + --danger-ink: #fca5a5; + --danger-line: #7f2a2a; + --danger-tint: #261010; + --teal-ink: #5eead4; + --teal-line: #1d5e57; + --teal-tint: #0b201e; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow: 0 4px 14px rgba(0, 0, 0, 0.45); + --shadow-lg: 0 18px 40px rgba(0, 0, 0, 0.55); +} + /* Author `display` declarations outrank the user-agent [hidden] rule, so a component style can silently un-hide an element. This makes `hidden` win. It exists because a plot canvas once stayed visible when it should not. */ @@ -60,10 +121,10 @@ h2 { font-size: 1.45rem; font-weight: 750; } a { color: var(--brand-2); text-underline-offset: 2px; } a:hover { color: var(--brand); } :focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; } -.skip-link { position: absolute; left: 0.75rem; top: -4rem; z-index: 100; padding: 0.6rem 1rem; border-radius: var(--radius-sm); background: var(--brand); color: #fff; font-weight: 650; text-decoration: none; } +.skip-link { position: absolute; left: 0.75rem; top: -4rem; z-index: 100; padding: 0.6rem 1rem; border-radius: var(--radius-sm); background: var(--brand); color: var(--on-brand); font-weight: 650; text-decoration: none; } .skip-link:focus { top: 0.75rem; } .app-main:focus { outline: none; } -::selection { background: #c7d2fe; } +::selection { background: var(--brand-line); } /* The system text cursor can be white and hard to see on a light page: use a white I-beam with a black outline. */ :root { --text-cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke-linecap='round'%3E%3Cpath d='M8 4h8M8 20h8M12 4v16' stroke='%23000' stroke-width='4'/%3E%3Cpath d='M8 4h8M8 20h8M12 4v16' stroke='%23fff' stroke-width='1.6'/%3E%3C/svg%3E") 12 12, text; } :where(p, li, h1, h2, h3, h4, h5, h6, td, th, pre, code, blockquote, figcaption, dd, dt, textarea, @@ -164,6 +225,7 @@ button { font: inherit; } .sidebar-lesson.complete .sidebar-lesson-dot { border-color: var(--success); background: var(--success); } .sidebar-lesson.active .sidebar-lesson-dot { box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.2); } .sidebar-lesson-icon { flex: none; width: 1.1rem; text-align: center; } +button.sidebar-lesson { width: 100%; border: 0; background: none; font: inherit; font-size: 0.88rem; text-align: left; cursor: pointer; } /* R status: an error sits in the page, where it cannot be missed; the busy and ready states float as a small pill so they never push the lesson down. */ @@ -173,13 +235,13 @@ button { font: inherit; } animation: pill-in var(--medium) var(--ease) both; } .r-status.ready::before, .r-status.busy::before { content: ""; width: 0.55rem; height: 0.55rem; border-radius: 50%; flex: none; } -.r-status.ready { color: #166534; } +.r-status.ready { color: var(--success-ink); } .r-status.ready::before { background: var(--success); box-shadow: 0 0 0 3px var(--success-soft); } -.r-status.busy { color: #92400e; } +.r-status.busy { color: var(--warn-ink); } .r-status.busy::before { background: var(--warn); animation: pulse 1.2s ease-in-out infinite; } .r-status.error { display: block; max-width: 48rem; margin: 0 auto 1.25rem; border-radius: var(--radius); padding: 0.9rem 1.1rem; - background: var(--danger-soft); color: #991b1b; border: 1px solid #fecaca; + background: var(--danger-soft); color: var(--danger-ink); border: 1px solid var(--danger-line); } .r-status.error p { margin: 0 0 0.5rem; } .r-status-detail { font-family: var(--mono); font-size: 0.75rem; word-break: break-all; } @@ -206,8 +268,8 @@ button { font: inherit; } .hero-eyebrow { display: inline-flex; align-items: center; gap: 0.45rem; margin: 0; padding: 0.25rem 0.7rem 0.25rem 0.35rem; border-radius: 999px; background: rgba(255, 255, 255, 0.16); font-size: 0.8rem; font-weight: 650; } .home-tagline { margin: 0 0 0.35rem; font-size: 1.15rem; font-weight: 600; color: #fff; } .hero-sub { margin: 0 0 1.4rem; max-width: 34rem; color: rgba(255, 255, 255, 0.85); font-size: 0.95rem; } -.hero .button-primary { background: #fff; color: var(--brand-ink); box-shadow: 0 8px 24px rgba(15, 23, 42, 0.25); } -.hero .button-primary:hover { color: var(--brand); } +.hero .button-primary { background: #fff; color: #3730a3; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.25); } +.hero .button-primary:hover { color: #4f46e5; } .hero-ring { position: relative; z-index: 1; } .progress-ring { width: 9.5rem; height: 9.5rem; } .progress-ring circle { fill: none; stroke-width: 11; } @@ -238,8 +300,8 @@ button { font: inherit; } padding: 1.1rem 1.15rem 0.9rem; border-radius: var(--radius); background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow-sm); transition: transform var(--medium) var(--ease), box-shadow var(--medium), border-color var(--medium); } -.module-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: #c7d2fe; } -.module-card.done { border-color: #bbf7d0; } +.module-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: var(--brand-line); } +.module-card.done { border-color: var(--success-line); } .module-card-head { display: flex; align-items: center; gap: 0.7rem; } .module-card-head h3 { margin: 0; font-size: 1.05rem; } .module-card-number { flex: none; display: grid; place-items: center; width: 2.1rem; height: 2.1rem; border-radius: 10px; background: var(--brand-soft); color: var(--brand-ink); font-weight: 800; } @@ -272,18 +334,18 @@ button { font: inherit; } .lesson h1 { margin: 0; } .lesson-chips { margin: 0.8rem 0 0; } .chip { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.7rem; border-radius: 999px; font-size: 0.8rem; font-weight: 650; background: var(--brand-soft); color: var(--brand-ink); transition: background var(--medium), color var(--medium); } -.chip-done { background: var(--success-soft); color: #166534; } +.chip-done { background: var(--success-soft); color: var(--success-ink); } .lesson { font-size: 1.04rem; color: var(--ink-2); } .lesson h2 { margin: 2.75rem 0 0.9rem; padding-top: 0.25rem; font-size: 1.5rem; } .lesson h2::before { content: ""; display: block; width: 2.5rem; height: 4px; margin-bottom: 0.75rem; border-radius: 999px; background: var(--gradient); } .lesson p, .lesson li { max-width: 42rem; } .lesson strong { color: var(--ink); } .lesson blockquote { - margin: 1.5rem 0; padding: 0.9rem 1.2rem; border-radius: var(--radius); border: 1px solid #dbe2ff; - color: var(--ink-2); background: linear-gradient(180deg, #f5f7ff, #eef1ff); + margin: 1.5rem 0; padding: 0.9rem 1.2rem; border-radius: var(--radius); border: 1px solid var(--brand-line); + color: var(--ink-2); background: linear-gradient(180deg, var(--brand-tint), var(--brand-tint)); } .lesson blockquote p { margin: 0.3rem 0; } -.lesson code { background: var(--surface-2); color: #3b3f9e; padding: 0.1rem 0.35rem; border-radius: 5px; font-family: var(--mono); font-size: 0.86em; overflow-wrap: anywhere; } +.lesson code { background: var(--surface-2); color: var(--brand-ink); padding: 0.1rem 0.35rem; border-radius: 5px; font-family: var(--mono); font-size: 0.86em; overflow-wrap: anywhere; } .lesson table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 1.25rem 0; } .lesson th, .lesson td { padding: 0.45rem 0.9rem 0.45rem 0; text-align: left; border-bottom: 1px solid var(--line); } .lesson th { color: var(--ink); font-size: 0.9rem; } @@ -311,7 +373,7 @@ button { font: inherit; } .lesson-finish { max-width: 48rem; margin: 3.5rem auto 0; padding: 1.4rem; border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow); } .page-lesson .lesson-finish { margin-left: max(0px, calc((100% - 65rem) / 2)); } -.lesson-finish.complete { border-color: #bbf7d0; background: linear-gradient(180deg, #f0fdf4, var(--surface) 70%); } +.lesson-finish.complete { border-color: var(--success-line); background: linear-gradient(180deg, var(--success-tint), var(--surface) 70%); } .lesson-finish-status { display: flex; gap: 0.9rem; align-items: center; } .lesson-finish-icon { font-size: 2rem; } .lesson-finish.complete .lesson-finish-icon { animation: bounce-in 700ms var(--ease) both; } @@ -364,7 +426,7 @@ button { font: inherit; } border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink-2); border-radius: 999px; padding: 0.35rem 0.8rem; font-size: 0.85rem; font-weight: 600; cursor: pointer; } -.model-index-filters button[aria-pressed='true'] { background: var(--brand); border-color: var(--brand); color: #fff; } +.model-index-filters button[aria-pressed='true'] { background: var(--brand); border-color: var(--brand); color: var(--on-brand); } .model-index-empty { color: var(--muted); padding: 1.5rem 0; } .model-index-section { margin-top: 1.6rem; } .model-index-section h3 { display: flex; align-items: center; gap: 0.5rem; margin: 0; font-size: 1.05rem; } @@ -392,7 +454,7 @@ button { font: inherit; } } .model-card-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: auto; padding-top: 0.2rem; } .model-card-tags .model-badge { font-size: 0.7rem; padding: 0.1rem 0.5rem; } -.model-chooser-answer { border: 1px solid #99f6e4; border-radius: var(--radius-lg); padding: 1.2rem 1.3rem; max-width: 36rem; background: linear-gradient(180deg, #f0fdfa, var(--surface)); box-shadow: var(--shadow); animation: pop-in var(--medium) var(--ease) both; } +.model-chooser-answer { border: 1px solid var(--teal-line); border-radius: var(--radius-lg); padding: 1.2rem 1.3rem; max-width: 36rem; background: linear-gradient(180deg, var(--teal-tint), var(--surface)); box-shadow: var(--shadow); animation: pop-in var(--medium) var(--ease) both; } .model-chooser-answer pre { background: #0f172a; color: #e2e8f0; padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); overflow-x: auto; margin: 0; } .model-chooser-answer code, .model-chooser-help code { overflow-wrap: anywhere; } .model-chooser-code { display: flex; flex-direction: column; align-items: flex-end; gap: 0.4rem; margin: 0.9rem 0; } @@ -403,9 +465,9 @@ button { font: inherit; } .model-chooser-trail li:not(:last-child)::after { content: '\203A'; margin-left: 0.4rem; } .model-chooser-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0 0 0.6rem; } .model-badge { display: inline-block; line-height: 1.4; font-size: 0.75rem; font-weight: 700; padding: 0.2rem 0.6rem; border-radius: 999px; border: 1px solid transparent; } -.model-badge.taught { background: #dcfce7; color: #166534; border-color: #86efac; } -.model-badge.beyond { background: #fef3c7; color: #92400e; border-color: #fcd34d; } -.model-badge.runs-here { background: var(--brand-soft); color: var(--brand-ink); border-color: #c7d2fe; } +.model-badge.taught { background: var(--success-soft); color: var(--success-ink); border-color: var(--success-line); } +.model-badge.beyond { background: var(--warn-soft); color: var(--warn-ink); border-color: var(--warn-line); } +.model-badge.runs-here { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand-line); } .model-badge.rstudio { background: var(--surface-2); color: var(--ink-2); border-color: var(--line-strong); } .link-button { border: none; background: none; color: var(--brand-2); text-decoration: underline; cursor: pointer; padding: 0; font-size: inherit; } @keyframes pop-in { from { opacity: 0; transform: translateY(8px) scale(0.98); } } @@ -454,7 +516,7 @@ button { font: inherit; } position: sticky; top: 0; z-index: 40; padding: 0.5rem max(0.75rem, env(safe-area-inset-right)) 0.5rem max(0.75rem, env(safe-area-inset-left)); padding-top: max(0.5rem, env(safe-area-inset-top)); - background: rgba(255, 255, 255, 0.82); backdrop-filter: saturate(1.6) blur(12px); -webkit-backdrop-filter: saturate(1.6) blur(12px); + background: color-mix(in srgb, var(--surface) 82%, transparent); backdrop-filter: saturate(1.6) blur(12px); -webkit-backdrop-filter: saturate(1.6) blur(12px); border-bottom: 1px solid var(--line); } .topbar-menu { diff --git a/src/components/AvatarTip.css b/src/components/AvatarTip.css index 1489243..0e467ea 100644 --- a/src/components/AvatarTip.css +++ b/src/components/AvatarTip.css @@ -2,19 +2,19 @@ .avatar-tip .avatar { flex: none; border-radius: 50%; box-shadow: var(--shadow-sm); } .avatar-tip-bubble { position: relative; flex: 1; min-width: 0; padding: 0.65rem 0.85rem; border-radius: var(--radius); - border: 1px solid #fcd34d; background: var(--warn-soft); color: #78350f; line-height: 1.45; + border: 1px solid var(--warn-line); background: var(--warn-soft); color: var(--warn-ink); line-height: 1.45; animation: pop-in var(--medium) var(--ease) both; } .avatar-tip-bubble::before { content: ""; position: absolute; left: -7px; top: 1rem; width: 12px; height: 12px; transform: rotate(45deg); background: inherit; border-left: inherit; border-bottom: inherit; } -.avatar-tip-error .avatar-tip-bubble { border-color: #fca5a5; background: var(--danger-soft); color: #7f1d1d; } +.avatar-tip-error .avatar-tip-bubble { border-color: var(--danger-line); background: var(--danger-soft); color: var(--danger-ink); } .avatar-tip-bubble p { margin: 0; } .avatar-tip-bubble p + p { margin-top: 0.4rem; } @media (prefers-reduced-motion: reduce) { .avatar-tip-bubble { animation: none; } } -.avatar-tip-coach .avatar-tip-bubble { border-color: #99f6e4; background: #f0fdfa; color: var(--ink); } -.avatar-tip-right .avatar-tip-bubble { border-color: #86efac; background: #f0fdf4; color: #14532d; } +.avatar-tip-coach .avatar-tip-bubble { border-color: var(--teal-line); background: var(--teal-tint); color: var(--ink); } +.avatar-tip-right .avatar-tip-bubble { border-color: var(--success-line); background: var(--success-tint); color: var(--success-ink); } .avatar-tip-title { font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.75; } .avatar-tip-bubble ul { margin: 0.2rem 0 0; padding-left: 1.2rem; } .avatar-tip-bubble li + li { margin-top: 0.3rem; } diff --git a/src/components/ChoiceBlock.css b/src/components/ChoiceBlock.css index a14b1ae..2c0cb53 100644 --- a/src/components/ChoiceBlock.css +++ b/src/components/ChoiceBlock.css @@ -1,12 +1,12 @@ .choice-block { - position: relative; border: 1px solid #dbe0ff; border-radius: var(--radius-lg); padding: 1.1rem 1.15rem; margin: 1.75rem 0; - background: linear-gradient(180deg, #f5f6ff, var(--surface) 6rem); box-shadow: var(--shadow-sm); + position: relative; border: 1px solid var(--brand-line); border-radius: var(--radius-lg); padding: 1.1rem 1.15rem; margin: 1.75rem 0; + background: linear-gradient(180deg, var(--brand-tint), var(--surface) 6rem); box-shadow: var(--shadow-sm); } -.choice-predict { border-color: #fde68a; background: linear-gradient(180deg, #fffbeb, var(--surface) 6rem); } -.choice-interpret { border-color: #99f6e4; background: linear-gradient(180deg, #f0fdfa, var(--surface) 6rem); } +.choice-predict { border-color: var(--warn-line); background: linear-gradient(180deg, var(--warn-tint), var(--surface) 6rem); } +.choice-interpret { border-color: var(--teal-line); background: linear-gradient(180deg, var(--teal-tint), var(--surface) 6rem); } .choice-label { display: inline-flex; align-items: center; gap: 0.35rem; margin: 0 0 0.45rem; padding: 0.15rem 0.6rem; border-radius: 999px; background: var(--brand-soft); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; color: var(--brand-ink); } -.choice-predict .choice-label { background: var(--warn-soft); color: #92400e; } -.choice-interpret .choice-label { background: var(--teal-soft); color: #0f766e; } +.choice-predict .choice-label { background: var(--warn-soft); color: var(--warn-ink); } +.choice-interpret .choice-label { background: var(--teal-soft); color: var(--teal-ink); } .choice-question { margin: 0 0 0.85rem; font-weight: 650; color: var(--ink); } .choice-options { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; } .choice-options button { @@ -24,11 +24,11 @@ } .choice-text { padding-top: 0.1rem; } .choice-options button.chosen { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18); } -.choice-options button.chosen .choice-letter { background: var(--brand); color: #fff; } -.choice-options button.is-correct { border-color: #4ade80; background: #f0fdf4; box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15); } +.choice-options button.chosen .choice-letter { background: var(--brand); color: var(--on-brand); } +.choice-options button.is-correct { border-color: var(--success-line); background: var(--success-tint); box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15); } .choice-options button.is-correct .choice-letter { background: var(--success); color: #fff; } .choice-options button.chosen.is-correct { animation: pop 420ms var(--ease); } -.choice-options button.is-wrong { border-color: #f87171; background: #fef2f2; box-shadow: none; animation: nudge 380ms ease-in-out; } +.choice-options button.is-wrong { border-color: var(--danger-line); background: var(--danger-tint); box-shadow: none; animation: nudge 380ms ease-in-out; } .choice-options button.is-wrong .choice-letter { background: var(--danger); color: #fff; } .points-pop { position: absolute; top: -0.7rem; right: 0.8rem; padding: 0.1rem 0.55rem; border-radius: 999px; font-size: 0.78rem; font-weight: 800; diff --git a/src/components/CodeBlock.css b/src/components/CodeBlock.css index 5cecb15..64b03da 100644 --- a/src/components/CodeBlock.css +++ b/src/components/CodeBlock.css @@ -1,14 +1,18 @@ .code-block { border: 1px solid var(--line); border-radius: var(--radius); padding: 0.8rem; margin: 1.5rem 0; background: var(--surface); box-shadow: var(--shadow-sm); } -.r-editor { border: 1px solid var(--line); border-radius: var(--radius-sm); overflow: hidden; background: #fbfcfe; transition: border-color var(--fast), box-shadow var(--fast); } +.r-editor { border: 1px solid #e4e7ef; border-radius: var(--radius-sm); overflow: hidden; background: #fbfcfe; color: #0f172a; transition: border-color var(--fast), box-shadow var(--fast); } .r-editor:focus-within { border-color: #a5b4fc; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); } .r-editor .cm-editor { max-height: 24rem; font-family: var(--mono); font-size: 0.88rem; } .r-editor .cm-gutters { background: #f4f6fb; border-right: 1px solid var(--line); color: #94a3b8; } .r-editor .cm-activeLine { background: rgba(99, 102, 241, 0.05); } .r-editor .cm-activeLineGutter { background: rgba(99, 102, 241, 0.08); } +/* Dark theme: CodeMirror's syntax colours are chosen for a light page, so turn the + light editor inside out. hue-rotate puts each colour back on its own hue. Its + colours above are literals, not tokens, so that they invert from light ones. */ +:root[data-theme="dark"] .r-editor { filter: invert(1) hue-rotate(180deg); } .code-block-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.65rem 0; } .code-block-actions button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 1.05rem; border-radius: 999px; - border: 1px solid var(--brand); background: var(--brand); color: #fff; cursor: pointer; font-size: 0.9rem; font-weight: 650; + border: 1px solid var(--brand); background: var(--brand); color: var(--on-brand); cursor: pointer; font-size: 0.9rem; font-weight: 650; transition: background var(--fast), transform var(--fast) var(--ease); } .code-block-actions button:not(.secondary)::before { content: "▶"; font-size: 0.65rem; } diff --git a/src/components/Credits.css b/src/components/Credits.css index 2f59771..ada0713 100644 --- a/src/components/Credits.css +++ b/src/components/Credits.css @@ -4,6 +4,8 @@ .credits-partner { display: inline-flex; align-items: center; color: var(--ink-2); text-decoration: none; } .credits-partner:hover .credits-wordmark { border-color: var(--brand); color: var(--brand-ink); } .credits-partner img { display: block; width: auto; max-width: 100%; } +/* The logos are dark on transparent: white on a dark page, hues kept. */ +:root[data-theme="dark"] .credits-partner img { filter: invert(1) hue-rotate(180deg); } .credits-wordmark { display: inline-block; padding: 0.3rem 0.65rem; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); font-weight: 700; letter-spacing: 0.01em; white-space: nowrap; transition: border-color var(--fast), color var(--fast); diff --git a/src/components/Exercise.css b/src/components/Exercise.css index 68880f2..7a21361 100644 --- a/src/components/Exercise.css +++ b/src/components/Exercise.css @@ -1,10 +1,10 @@ .exercise { - position: relative; border: 1px solid #99f6e4; border-radius: var(--radius-lg); padding: 1.15rem; margin: 2rem 0; - background: linear-gradient(180deg, #f0fdfa, var(--surface) 7rem); box-shadow: var(--shadow); + position: relative; border: 1px solid var(--teal-line); border-radius: var(--radius-lg); padding: 1.15rem; margin: 2rem 0; + background: linear-gradient(180deg, var(--teal-tint), var(--surface) 7rem); box-shadow: var(--shadow); transition: border-color var(--medium), box-shadow var(--medium); } -.exercise-passed { border-color: #86efac; box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12), var(--shadow); } -.exercise-label { display: inline-flex; align-items: center; gap: 0.35rem; margin: 0 0 0.45rem; padding: 0.15rem 0.6rem; border-radius: 999px; background: var(--teal-soft); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; color: #0f766e; } +.exercise-passed { border-color: var(--success-line); box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12), var(--shadow); } +.exercise-label { display: inline-flex; align-items: center; gap: 0.35rem; margin: 0 0 0.45rem; padding: 0.15rem 0.6rem; border-radius: 999px; background: var(--teal-soft); font-size: 0.72rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; color: var(--teal-ink); } .exercise-prompt { margin: 0 0 0.85rem; font-weight: 650; color: var(--ink); } .exercise-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.75rem 0; } .exercise-actions button { @@ -13,20 +13,20 @@ } .exercise-actions button:hover:not(:disabled) { background: #0f766e; } .exercise-actions button:active:not(:disabled) { transform: scale(0.97); } -.exercise-actions button.secondary { background: var(--surface); color: #0f766e; border-color: #99f6e4; } +.exercise-actions button.secondary { background: var(--surface); color: var(--teal-ink); border-color: var(--teal-line); } .exercise-actions button.secondary:hover:not(:disabled) { background: var(--teal-soft); } .exercise-actions button:disabled { opacity: 0.5; cursor: not-allowed; } .exercise-hints { margin: 0; } .exercise-hints li { animation: pop-in var(--medium) var(--ease) both; } .exercise-outcome { padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); margin: 0.7rem 0; animation: pop-in var(--medium) var(--ease) both; } .exercise-outcome p { margin: 0; } -.outcome-pass { background: var(--success-soft); border: 1px solid #86efac; color: #14532d; font-weight: 600; } +.outcome-pass { background: var(--success-soft); border: 1px solid var(--success-line); color: var(--success-ink); font-weight: 600; } .outcome-pass p::before { content: "✓ "; } -.outcome-fail { background: var(--warn-soft); border: 1px solid #fcd34d; color: #78350f; animation-name: nudge; } -.outcome-student-error { background: var(--danger-soft); border: 1px solid #fca5a5; color: #7f1d1d; } +.outcome-fail { background: var(--warn-soft); border: 1px solid var(--warn-line); color: var(--warn-ink); animation-name: nudge; } +.outcome-student-error { background: var(--danger-soft); border: 1px solid var(--danger-line); color: var(--danger-ink); } .outcome-broken-check { background: #ede9fe; border: 1px solid #c4b5fd; color: #4c1d95; } .exercise-solution { background: #0f172a; color: #e2e8f0; padding: 0.8rem; border-radius: var(--radius-sm); overflow-x: auto; font-family: var(--mono); font-size: 0.86rem; animation: pop-in var(--medium) var(--ease) both; } -.exercise-missing { color: var(--danger); font-weight: 600; } +.exercise-missing { color: var(--danger-ink); font-weight: 600; } @keyframes nudge { 0%, 100% { transform: none; } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } } .exercise-task .exercise-prompt { margin: 0; } .exercise-advice { font-size: 0.9rem; color: var(--muted); } diff --git a/src/components/FileUpload.css b/src/components/FileUpload.css index fd232c4..a91fe43 100644 --- a/src/components/FileUpload.css +++ b/src/components/FileUpload.css @@ -2,7 +2,7 @@ .file-upload-compact { margin: 0.5rem 0 0.75rem; padding: 0.55rem 0.7rem; } .file-upload-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; } .file-upload button { - padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--brand); background: var(--brand); color: #fff; + padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--brand); background: var(--brand); color: var(--on-brand); cursor: pointer; font-size: 0.88rem; font-weight: 650; transition: background var(--fast); } .file-upload button:hover:not(:disabled) { background: var(--brand-ink); } @@ -10,7 +10,7 @@ .file-upload button.secondary { background: var(--surface); color: var(--brand-ink); border-color: var(--line-strong); padding: 0.25rem 0.75rem; font-size: 0.82rem; } .file-upload button.secondary:hover:not(:disabled) { background: var(--brand-soft); } .file-upload-note { color: var(--muted); font-size: 0.85rem; flex: 1 1 16rem; } -.file-upload-error { margin: 0.6rem 0 0; padding: 0.5rem 0.75rem; border-radius: var(--radius-sm); background: var(--danger-soft); border: 1px solid #fca5a5; color: #7f1d1d; font-size: 0.88rem; } +.file-upload-error { margin: 0.6rem 0 0; padding: 0.5rem 0.75rem; border-radius: var(--radius-sm); background: var(--danger-soft); border: 1px solid var(--danger-line); color: var(--danger-ink); font-size: 0.88rem; } .file-upload-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: grid; gap: 0.45rem; } .file-upload-list li { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.6rem; padding: 0.45rem 0.6rem; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--line); } .file-upload-name { font-weight: 650; color: var(--ink); font-size: 0.88rem; } diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index bcd3cc1..2e9d232 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -86,3 +86,32 @@ describe('Sidebar', () => { expect(link.className).toContain('complete'); }); }); + +test('the dark mode button switches the theme and remembers it', () => { + document.documentElement.dataset.theme = 'light'; + renderSidebar(); + const button = screen.getByRole('button', { name: 'Dark mode' }); + expect(button.getAttribute('aria-pressed')).toBe('false'); + act(() => button.click()); + expect(document.documentElement.dataset.theme).toBe('dark'); + expect(localStorage.getItem('statlab.theme')).toBe('dark'); + expect(button.getAttribute('aria-pressed')).toBe('true'); + act(() => button.click()); + expect(document.documentElement.dataset.theme).toBe('light'); +}); + +test('until a theme is picked, the page follows the system setting as it changes', () => { + localStorage.removeItem('statlab.theme'); + document.documentElement.dataset.theme = 'light'; + let listener: ((event: { matches: boolean }) => void) | undefined; + vi.stubGlobal('matchMedia', () => ({ matches: false, addEventListener: (_: string, fn: typeof listener) => { listener = fn; }, removeEventListener: () => {} })); + renderSidebar(); + act(() => listener?.({ matches: true })); + expect(document.documentElement.dataset.theme).toBe('dark'); + expect(screen.getByRole('button', { name: 'Dark mode' }).getAttribute('aria-pressed')).toBe('true'); + // A choice the student made wins over the system. + localStorage.setItem('statlab.theme', 'dark'); + act(() => listener?.({ matches: false })); + expect(document.documentElement.dataset.theme).toBe('dark'); + vi.unstubAllGlobals(); +}); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 86c6374..3c39639 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -9,6 +9,12 @@ import Avatar from './Avatar'; import Credits from './Credits'; import Logo from './Logo'; +/** Read by the script in index.html, which applies the theme before React loads. */ +const THEME_KEY = 'statlab.theme'; +/** The browser bar colour for each theme, the same as in index.html. */ +const LIGHT_BAR = '#f8fafc'; +const DARK_BAR = '#0b1120'; + type Props = { /** Whether the drawer is showing, on screens narrow enough to have one. */ open?: boolean; @@ -22,6 +28,28 @@ export default function Sidebar({ open = false }: Props) { // useSyncExternalStore snapshot; a counter re-renders on each store write. useEffect(() => subscribeProgress(() => setTick((tick) => tick + 1)), []); const progress = getProgress(); + const [dark, setDark] = useState(() => document.documentElement.dataset.theme === 'dark'); + function applyTheme(isDark: boolean) { + document.documentElement.dataset.theme = isDark ? 'dark' : 'light'; + document.querySelector('meta[name="theme-color"]')?.setAttribute('content', isDark ? DARK_BAR : LIGHT_BAR); + setDark(isDark); + } + function toggleTheme() { + try { localStorage.setItem(THEME_KEY, dark ? 'light' : 'dark'); } catch { /* Private mode: it still switches for this visit. */ } + applyTheme(!dark); + } + // Until the student picks a theme, follow the system setting as it changes. + useEffect(() => { + const query = window.matchMedia?.('(prefers-color-scheme: dark)'); + if (!query) return; + function onChange(event: MediaQueryListEvent) { + let saved = null; + try { saved = localStorage.getItem(THEME_KEY); } catch { /* Treated as no choice made. */ } + if (!saved) applyTheme(event.matches); + } + query.addEventListener('change', onChange); + return () => query.removeEventListener('change', onChange); + }, []); const stats = courseStats(progress); // One module open at a time by default: the one holding the current lesson. @@ -122,6 +150,7 @@ export default function Sidebar({ open = false }: Props) {
  • How many participants?
  • Review quiz
  • R Workspace
  • +
  • diff --git a/src/components/workspace/Workspace.css b/src/components/workspace/Workspace.css index a1803e6..b2a5943 100644 --- a/src/components/workspace/Workspace.css +++ b/src/components/workspace/Workspace.css @@ -13,6 +13,7 @@ --ide-head: #eef1f6; --ide-edge: #d5dbe5; --ide-echo: #1d4ed8; + --ide-well: #e3e8ef; margin: 1rem 0 0; } @@ -25,7 +26,7 @@ box-sizing: border-box; border: 1px solid var(--ide-edge); border-radius: var(--radius); - background: #e3e8ef; + background: var(--ide-well); box-shadow: var(--shadow-sm); } .ide-column { display: flex; flex-direction: column; min-width: 0; min-height: 0; flex: 1 1 0; } @@ -65,7 +66,7 @@ body.ide-dragging-y { cursor: row-resize; } .ide-tab .ide-tab-close { padding: 0.2rem 0.45rem 0.2rem 0; font-size: 1rem; line-height: 1; } .ide-tab-title { padding: 0.3rem 0.65rem; font-size: 0.82rem; font-weight: 650; color: var(--ink); background: var(--surface); border: 1px solid var(--ide-edge); border-bottom: 0; border-radius: 5px 5px 0 0; margin-bottom: -1px; } .ide-tabs-aside { margin-left: auto; display: inline-flex; align-items: center; gap: 0.5rem; padding-bottom: 0.25rem; font-size: 0.78rem; } -.ide-busy { color: #92400e; font-weight: 600; } +.ide-busy { color: var(--warn-ink); font-weight: 600; } .ide-busy::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; margin-right: 0.35rem; border-radius: 50%; background: var(--warn); animation: pulse 1.2s ease-in-out infinite; } .ide-panel { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; } @@ -80,7 +81,7 @@ body.ide-dragging-y { cursor: row-resize; } } .ide-toolbar button:hover:not(:disabled), .ide-tab-action:hover, .ide-file-actions button:hover { background: var(--brand-soft); border-color: var(--brand); color: var(--brand-ink); } .ide-toolbar button:disabled { opacity: 0.45; cursor: not-allowed; } -.ide-toolbar button.danger { border-color: var(--danger); color: var(--danger); } +.ide-toolbar button.danger { border-color: var(--danger); color: var(--danger-ink); } .ide-pane-source .ide-toolbar button:first-child:not(:disabled) { border-color: var(--brand); color: var(--brand-ink); } .ide-pane-source .ide-toolbar button:first-child::before { content: "▶ "; font-size: 0.65rem; } .ide-toolbar input[type="search"] { @@ -92,14 +93,14 @@ body.ide-dragging-y { cursor: row-resize; } .ide-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; } .ide-empty { margin: 0; padding: 0.75rem; color: var(--muted); font-size: 0.85rem; } -.ide-error { flex: none; margin: 0.4rem; padding: 0.4rem 0.6rem; border-radius: 6px; background: var(--danger-soft); border: 1px solid #fca5a5; color: #7f1d1d; font-size: 0.82rem; } +.ide-error { flex: none; margin: 0.4rem; padding: 0.4rem 0.6rem; border-radius: 6px; background: var(--danger-soft); border: 1px solid var(--danger-line); color: var(--danger-ink); font-size: 0.82rem; } .ide-link { border: 0; background: none; padding: 0; font: inherit; color: var(--brand-2); cursor: pointer; text-align: left; } .ide-link:hover:not(:disabled) { text-decoration: underline; } .ide-link[aria-current] { color: var(--ink); font-weight: 650; cursor: default; } /* Source --------------------------------------------------------------------- */ .ide-editor { flex: 1 1 auto; min-height: 0; display: flex; } -.ide-editor .r-editor { flex: 1; min-width: 0; border: 0; border-radius: 0; background: var(--surface); } +.ide-editor .r-editor { flex: 1; min-width: 0; border: 0; border-radius: 0; background: #fff; } .ide-editor .r-editor:focus-within { box-shadow: none; } .ide-editor .r-editor, .ide-editor .cm-editor { height: 100%; } .ide-editor .r-editor .cm-editor { max-height: none; } @@ -112,8 +113,8 @@ body.ide-dragging-y { cursor: row-resize; } } .ide-console-line { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); } .ide-console-echo { color: var(--ide-echo); } -.ide-console-error { color: #b91c1c; } -.ide-console-warning, .ide-console-stderr { color: #b45309; } +.ide-console-error { color: var(--danger-ink); } +.ide-console-warning, .ide-console-stderr { color: var(--warn-ink); } .ide-console-message { color: var(--muted); } .ide-console-prompt { display: flex; gap: 0.5ch; color: var(--ide-echo); } .ide-console-prompt textarea { @@ -197,7 +198,7 @@ body.ide-dragging-y { cursor: row-resize; } @container ide (max-width: 640px) { .ide-switcher { display: flex; gap: 2px; padding: 3px; margin-bottom: 0.5rem; - border-radius: 10px; background: #e3e8ef; + border-radius: 10px; background: var(--ide-well); } .ide-switcher button { flex: 1 1 auto; min-width: 0; min-height: 40px; padding: 0.3rem 0.35rem; border: 0; border-radius: 8px; background: transparent; @@ -224,3 +225,5 @@ body.ide-dragging-y { cursor: row-resize; } .ide-install input { font-size: 16px; } .ide-console-prompt textarea, .ide-toolbar input[type="search"] { font-size: 16px; } } + +:root[data-theme="dark"] .ide { --ide-head: #1c2436; --ide-edge: #2c3650; --ide-echo: #93c5fd; --ide-well: #0b1120; } diff --git a/src/pages/AvatarStudio.css b/src/pages/AvatarStudio.css index 2a2b24f..c2cc672 100644 --- a/src/pages/AvatarStudio.css +++ b/src/pages/AvatarStudio.css @@ -43,7 +43,7 @@ border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); box-shadow: var(--shadow-sm); text-align: center; } .avatar-card .avatar { border-radius: 50%; } -.avatar-card.owned { background: var(--success-soft); border-color: #bbf7d0; } +.avatar-card.owned { background: var(--success-soft); border-color: var(--success-line); } .avatar-card .button { padding: 0.4rem 0.9rem; font-size: 0.88rem; } .avatar-card .button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; } .avatar-short { font-size: 0.75rem; color: var(--muted); } diff --git a/src/pages/ModelChooser.css b/src/pages/ModelChooser.css index 098d78d..a9f5640 100644 --- a/src/pages/ModelChooser.css +++ b/src/pages/ModelChooser.css @@ -14,7 +14,7 @@ .mc-kicker { margin: 0; font-size: 0.72rem; font-weight: 750; letter-spacing: 0.09em; text-transform: uppercase; color: var(--brand); } /* The research-question box ------------------------------------------------ */ -.mc-matcher { background: linear-gradient(180deg, var(--brand-soft), var(--surface) 11rem); border-color: #dfe3ff; } +.mc-matcher { background: linear-gradient(180deg, var(--brand-soft), var(--surface) 11rem); border-color: var(--brand-line); } .mc-matcher textarea { display: block; width: 100%; box-sizing: border-box; resize: vertical; min-height: 4.2rem; font: inherit; font-size: 1.05rem; line-height: 1.5; color: var(--ink); @@ -22,7 +22,7 @@ transition: border-color var(--fast), box-shadow var(--fast); } .mc-matcher textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.15); } -.mc-matcher textarea::placeholder { color: #94a3b8; } +.mc-matcher textarea::placeholder { color: var(--muted); } .mc-examples { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); gap: 0.45rem; margin: 0.9rem 0 0; } .mc-examples-label { grid-column: 1 / -1; font-size: 0.85rem; font-weight: 650; color: var(--muted); } @@ -46,7 +46,7 @@ transition: background var(--fast), border-color var(--fast), color var(--fast); } .mc-chip::before { content: '+'; font-weight: 800; } -.mc-chip[aria-pressed='true'] { border: 1px solid #99f6e4; background: var(--teal-soft); color: #115e59; } +.mc-chip[aria-pressed='true'] { border: 1px solid var(--teal-line); background: var(--teal-soft); color: var(--teal-ink); } .mc-chip[aria-pressed='true']::before { content: '\2713'; } .mc-chip[aria-pressed='true']:hover { text-decoration: line-through; } .mc-chip[aria-pressed='false']:hover { border-color: var(--brand); color: var(--brand-ink); } @@ -60,8 +60,8 @@ display: flex; flex-direction: column; gap: 0.35rem; animation: pop-in var(--medium) var(--ease) both; } .mc-suggestion.best { - grid-column: 1 / -1; padding: 1.2rem 1.3rem; border: 1.5px solid #a5b4fc; box-shadow: var(--shadow); - background: linear-gradient(135deg, #f5f7ff, var(--surface) 60%); + grid-column: 1 / -1; padding: 1.2rem 1.3rem; border: 1.5px solid var(--brand-line); box-shadow: var(--shadow); + background: linear-gradient(135deg, var(--brand-tint), var(--surface) 60%); } .mc-suggestion h3 { margin: 0; font-size: 1.05rem; } .mc-suggestion.best h3 { font-size: 1.35rem; } @@ -75,7 +75,7 @@ .mc-link-button { background: none; border: 0; padding: 0.3rem 0; cursor: pointer; color: var(--brand-2); font-weight: 600; font-size: 0.9rem; text-decoration: underline; text-underline-offset: 3px; } .mc-link-button:hover { color: var(--brand); } .mc-suggestion-detail:not([hidden]) { margin-top: 0.6rem; padding-top: 0.9rem; border-top: 1px solid var(--line); } -.mc-empty { margin: 1.2rem 0 0; padding: 0.9rem 1rem; border-radius: var(--radius); background: var(--warn-soft); color: #78350f; font-size: 0.95rem; } +.mc-empty { margin: 1.2rem 0 0; padding: 0.9rem 1rem; border-radius: var(--radius); background: var(--warn-soft); color: var(--warn-ink); font-size: 0.95rem; } /* The step-by-step questions ---------------------------------------------- */ .model-chooser .model-chooser-trail { @@ -124,7 +124,7 @@ .mc-option-example { font-size: 0.88rem; line-height: 1.45; color: var(--ink-2); font-style: italic; } .mc-option-example::before { content: 'e.g. '; font-style: normal; font-weight: 650; color: var(--muted); } .mc-option-tech { font-size: 0.72rem; font-weight: 650; letter-spacing: 0.02em; color: var(--muted); } -.mc-flag { align-self: flex-start; order: -1; font-size: 0.7rem; font-weight: 750; color: #fff; background: var(--brand); border-radius: 999px; padding: 0.05rem 0.55rem; } +.mc-flag { align-self: flex-start; order: -1; font-size: 0.7rem; font-weight: 750; color: var(--on-brand); background: var(--brand); border-radius: 999px; padding: 0.05rem 0.55rem; } .model-chooser .model-chooser-answer { max-width: none; margin-top: 0.4rem; } .model-chooser .model-chooser-legend { margin: 1.4rem 0 0; font-size: 0.85rem; line-height: 1.55; max-width: none; } diff --git a/src/pages/SampleSize.css b/src/pages/SampleSize.css index 1254021..6049a31 100644 --- a/src/pages/SampleSize.css +++ b/src/pages/SampleSize.css @@ -1,6 +1,6 @@ /* Sample size planner. Builds on the panels in ModelChooser.css. */ -.ss-tag { align-self: flex-start; font-size: 0.72rem; font-weight: 750; color: #065f46; background: #d1fae5; border-radius: 999px; padding: 0.05rem 0.55rem; } +.ss-tag { align-self: flex-start; font-size: 0.72rem; font-weight: 750; color: var(--teal-ink); background: var(--teal-soft); border-radius: 999px; padding: 0.05rem 0.55rem; } .ss-option-note { font-size: 0.9rem; line-height: 1.45; color: var(--ink-2); } .ss-input-step { margin-top: 0.4rem; } .ss-input-step .button-primary { margin-top: 0.4rem; } @@ -18,12 +18,12 @@ .ss-field small { color: var(--muted); font-size: 0.85rem; line-height: 1.4; } .ss-meaning { color: var(--ink-2); background: var(--surface-2); border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; } -.ss-result { border-color: #c7d2fe; } +.ss-result { border-color: var(--brand-line); } .ss-result h4 { margin: 1.4rem 0 0.5rem; font-size: 1.05rem; } .ss-headline { display: flex; flex-wrap: wrap; gap: 0.6rem 2rem; margin: 0.3rem 0 0.8rem; } .ss-headline p { margin: 0; color: var(--ink-2); font-weight: 600; } .ss-big { display: block; font-size: 2.6rem; font-weight: 800; line-height: 1.1; color: var(--brand-ink); } -.ss-error { color: #7f1d1d; background: var(--danger-soft); border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; } +.ss-error { color: var(--danger-ink); background: var(--danger-soft); border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; } .ss-note { color: var(--muted); font-size: 0.92rem; } .ss-report { background: var(--surface-2); border-left: 4px solid var(--brand); padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); line-height: 1.55; } .ss-table { border-collapse: collapse; margin: 0.3rem 0; font-size: 0.95rem; } @@ -36,7 +36,7 @@ .ss-grid { stroke: var(--line); } .ss-target { stroke: var(--warn); stroke-dasharray: 5 4; stroke-width: 1.5; } .ss-line { fill: none; stroke: var(--brand); stroke-width: 2.5; } -.ss-mark { fill: var(--brand); stroke: #fff; stroke-width: 2; } +.ss-mark { fill: var(--brand); stroke: var(--surface); stroke-width: 2; } .ss-result pre { background: #0f172a; color: #e2e8f0; padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); overflow-x: auto; margin: 0; max-width: 100%; box-sizing: border-box; } .ss-result .model-chooser-code { min-width: 0; } -.ss-warning { color: var(--ink-2); background: #fef3c7; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; } +.ss-warning { color: var(--ink-2); background: var(--warn-soft); border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; } diff --git a/src/sims/CI.css b/src/sims/CI.css index e972296..33b4301 100644 --- a/src/sims/CI.css +++ b/src/sims/CI.css @@ -7,8 +7,8 @@ .ci-svg { width: 100%; height: auto; background: #f8fafc; border-radius: 4px; } .ci-svg line { cursor: pointer; } .ci-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.5rem; } -.ci-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.ci-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .ci-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } -.ci-single { margin: 0.5rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid #1d4ed8; background: #eff6ff; font-size: 0.9rem; } -.ci-note { margin: 0.5rem 0 0; color: #475569; font-size: 0.85rem; } +.ci-single { margin: 0.5rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid #1d4ed8; background: var(--brand-tint); font-size: 0.9rem; } +.ci-note { margin: 0.5rem 0 0; color: var(--muted); font-size: 0.85rem; } diff --git a/src/sims/CLT.css b/src/sims/CLT.css index 90baa37..08aa179 100644 --- a/src/sims/CLT.css +++ b/src/sims/CLT.css @@ -3,12 +3,12 @@ .clt-controls label { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; } .clt-slider input { width: 12rem; } .clt-controls button { padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--brand-ink); cursor: pointer; font-weight: 600; transition: background var(--fast), border-color var(--fast); } -.clt-description { margin: 0 0 0.75rem; color: #475569; font-size: 0.9rem; } +.clt-description { margin: 0 0 0.75rem; color: var(--muted); font-size: 0.9rem; } .clt-figure { margin: 0 0 0.75rem; } -.clt-figure figcaption { font-size: 0.85rem; color: #334155; margin-bottom: 0.2rem; } +.clt-figure figcaption { font-size: 0.85rem; color: var(--ink-2); margin-bottom: 0.2rem; } .clt-svg { width: 100%; height: auto; background: #f8fafc; border-radius: 4px; } .clt-axis { font-size: 11px; fill: #475569; } .clt-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; } -.clt-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.clt-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .clt-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } diff --git a/src/sims/Correlation.css b/src/sims/Correlation.css index bd2476f..40688f5 100644 --- a/src/sims/Correlation.css +++ b/src/sims/Correlation.css @@ -5,10 +5,10 @@ .correlation-slider input[type="range"] { width: 11rem; } .correlation-slider span { font-variant-numeric: tabular-nums; min-width: 3rem; } .correlation-controls button { padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--brand-ink); cursor: pointer; font-weight: 600; transition: background var(--fast), border-color var(--fast); } -.correlation-controls button:disabled { border-color: #cbd5e1; color: #94a3b8; cursor: default; } +.correlation-controls button:disabled { border-color: var(--line-strong); color: var(--muted); cursor: default; } .correlation-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.5rem; } -.correlation-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.correlation-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .correlation-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } -.correlation-score { margin: 0.5rem 0 0; color: #475569; font-size: 0.85rem; } -.correlation-verdict { margin: 0.35rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid #b45309; background: #fffbeb; font-size: 0.88rem; } +.correlation-score { margin: 0.5rem 0 0; color: var(--muted); font-size: 0.85rem; } +.correlation-verdict { margin: 0.35rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid var(--warn-ink); background: var(--warn-tint); font-size: 0.88rem; } diff --git a/src/sims/Distribution.css b/src/sims/Distribution.css index 1cac616..f9b56a3 100644 --- a/src/sims/Distribution.css +++ b/src/sims/Distribution.css @@ -6,7 +6,7 @@ .distribution-svg { width: 100%; height: auto; background: #f8fafc; border-radius: 4px; } .distribution-axis { font-size: 11px; fill: #475569; } .distribution-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.5rem; } -.distribution-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.distribution-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .distribution-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } -.distribution-note { margin: 0.5rem 0 0; color: #475569; font-size: 0.85rem; } +.distribution-note { margin: 0.5rem 0 0; color: var(--muted); font-size: 0.85rem; } diff --git a/src/sims/LeastSquares.css b/src/sims/LeastSquares.css index f540a90..16e1f59 100644 --- a/src/sims/LeastSquares.css +++ b/src/sims/LeastSquares.css @@ -6,10 +6,10 @@ .leastsquares-slider input[type="range"] { width: 10rem; } .leastsquares-slider span { font-variant-numeric: tabular-nums; min-width: 3rem; } .leastsquares-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.5rem; } -.leastsquares-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.leastsquares-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .leastsquares-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } .leastsquares-equation { margin: 0.5rem 0 0; font-size: 0.9rem; } .leastsquares-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 0.75rem; } .leastsquares-actions button { padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--brand-ink); cursor: pointer; font-weight: 600; transition: background var(--fast), border-color var(--fast); } -.leastsquares-note { margin: 0.75rem 0 0; color: #475569; font-size: 0.85rem; } +.leastsquares-note { margin: 0.75rem 0 0; color: var(--muted); font-size: 0.85rem; } diff --git a/src/sims/PValue.css b/src/sims/PValue.css index cb11628..b75202b 100644 --- a/src/sims/PValue.css +++ b/src/sims/PValue.css @@ -4,12 +4,12 @@ .pvalue-slider input[type="range"] { width: 9rem; } .pvalue-slider span { font-variant-numeric: tabular-nums; min-width: 2.5rem; } .pvalue-controls button { padding: 0.4rem 0.95rem; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--brand-ink); cursor: pointer; font-weight: 600; transition: background var(--fast), border-color var(--fast); } -.pvalue-caption { margin: 0 0 0.5rem; color: #475569; font-size: 0.85rem; } +.pvalue-caption { margin: 0 0 0.5rem; color: var(--muted); font-size: 0.85rem; } .pvalue-svg { width: 100%; height: auto; background: #f8fafc; border-radius: 4px; } .pvalue-axis { font-size: 11px; fill: #475569; } .pvalue-readout { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin-top: 0.5rem; } -.pvalue-readout th { text-align: left; font-weight: 500; color: #475569; padding: 0.2rem 0.5rem 0.2rem 0; } +.pvalue-readout th { text-align: left; font-weight: 500; color: var(--muted); padding: 0.2rem 0.5rem 0.2rem 0; } .pvalue-readout td { padding: 0.2rem 1.25rem 0.2rem 0; font-variant-numeric: tabular-nums; font-weight: 600; } .pvalue-verdict { margin: 0.5rem 0 0; font-weight: 600; } -.pvalue-caveat { margin: 0.35rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid #b91c1c; background: #fef2f2; font-size: 0.88rem; } +.pvalue-caveat { margin: 0.35rem 0 0; padding: 0.5rem 0.75rem; border-left: 3px solid var(--danger-ink); background: var(--danger-tint); font-size: 0.88rem; }