/* * Theme tokens — single source of truth for colours is src/lib/theme.ts. * These custom properties mirror its `cssVars` map; keep the two in sync. */ :root { --color-gold: #ffd700; --color-experience: #4ecdc4; --color-projects: #ff6b6b; --color-skills: #f9a825; --color-education: #dda0dd; --bg-space: #05050a; --panel-from: #1a1a2e; --panel-to: #0f0f1a; /* * Text colours, with measured contrast against --panel-from (#1a1a2e): * --text-primary 14.0:1 * --text-muted 7.3:1 * --text-subtle 6.1:1 * The previous #666 used for hints, counts and the modal close glyph was * 2.97:1 — below the 4.5:1 AA threshold for normal text. */ --text-primary: #e8e8f0; --text-muted: #a8a8b8; --text-subtle: #9a9aa8; /* * Type. Cinzel is a Trajan-style inscriptional face — display sizes only; * body copy stays on the system stack, which costs nothing to load and is * far more legible at 13-14px. Before this, the single font-family * declaration in the codebase named two fonts that were never loaded, so * every word rendered in the browser's default serif. */ --font-display: 'Cinzel Variable', 'Cinzel', 'Trajan Pro', Georgia, serif; --font-body: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; /* Spacing scale */ --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem; --space-5: 1.5rem; --space-6: 2rem; /* Radii */ --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px; /* * Layering. These were ad-hoc literals scattered across seven files, with * LoadingIndicator and the modal backdrop both sitting on 1000. */ --z-welcome: 100; --z-tooltip: 500; --z-panel-backdrop: 899; --z-panel: 900; --z-panel-toggle: 950; --z-modal: 1000; --z-toast: 1100; --z-update-toast: 1150; --z-tree-nav: 1200; --z-skip-link: 2000; /* Motion */ --ease-out: cubic-bezier(0.4, 0, 0.2, 1); --duration-fast: 0.15s; --duration-base: 0.3s; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html, body { width: 100%; height: 100%; font-family: var(--font-body); color: var(--text-primary); margin: 0; padding: 0; overflow: hidden; background: var(--bg-space); } /* * Focus indicators. These used to live in SkillTree.svelte's *scoped* style * block, so Svelte's scoping meant they never reached NodeModal or StatsPanel * children — those fell back to the UA default outline, which is low-contrast * on this background. Global is the only place this rule works. */ :focus-visible { outline: 2px solid var(--color-gold); outline-offset: 2px; border-radius: 2px; } /* * Reduced motion. The JS side (see config.prefersReducedMotion) skips the * entrance and never creates the ambient GSAP loops; this covers the CSS * animations and transitions those can't reach. */ @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; } } /* * Visually-hidden but available to screen readers and crawlers. Used for the * text version of the portfolio (the interactive content is canvas-rendered). */ /* * ...but focusable content inside it must become visible when focused, or a * sighted keyboard user tabs through ~20 invisible stops with no idea where * focus is (WCAG 2.4.7). Standard sr-only-focusable treatment. */ .sr-only:focus-within { position: fixed; top: 0; left: 0; z-index: var(--z-skip-link); width: auto; height: auto; max-width: min(90vw, 60rem); max-height: 90vh; margin: 0; overflow: auto; clip: auto; white-space: normal; padding: 1.5rem; background: linear-gradient(145deg, var(--panel-from) 0%, var(--panel-to) 100%); border: 1px solid var(--color-gold); border-radius: 0 0 8px 0; color: #e8e8f0; line-height: 1.6; } .sr-only:focus-within a { color: var(--color-gold); } .sr-only:focus-within h1, .sr-only:focus-within h2, .sr-only:focus-within h3 { font-family: var(--font-display); letter-spacing: 0.02em; color: var(--color-gold); margin: 1rem 0 0.5rem; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } /* * Un-hides the text version when the canvas can't run — no JS, or no WebGL. * Applied by