/** * global.css * --------------------------------------------------------------------------- * Tailwind v4 entry + global base layer for Greg Kilmer's profile site. * * Tailwind v4 uses a CSS-first config. The `@import "tailwindcss"` pulls in the * engine and the `@theme` block below exposes the project palette as utility * classes (e.g. `bg-surface`, `text-accent`). Section/light-dark tokens live in * themes.css and are referenced here via CSS variables. * * Import order matters: theme tokens first (so @theme can reference them), then * Tailwind, then animations, then base. */ /* Palette + light/dark + section surface tokens (CSS variables). */ @import "./themes.css"; /* Keyframes (gradient shift, fade-up, blur, float). */ @import "./animations.css"; /* Tailwind engine. In v4 this is a single import. */ @import "tailwindcss"; /* --------------------------------------------------------------------------- * Theme bridge: expose CSS variables from themes.css to Tailwind utilities. * Each entry below generates a utility family, e.g. `--color-surface` -> the * `bg-surface` / `text-surface` / `border-surface` classes. * The values point at the CSS variables so that changing `data-theme` or the * section wrapper (`data-surface`) instantly restyles every component. * ------------------------------------------------------------------------- */ @theme { /* Brand colors (fixed, do not change with theme). Night-sky mystical set: * star gold, mystic violet, nebula teal, midnight indigo, star-white. */ --color-amber: #e8b54a; /* star gold */ --color-amber-soft: #f4cd72; /* brighter gold for hovers */ --color-violet: #8b7bd8; /* mystic violet */ --color-violet-deep: #6f5fb6; --color-teal: #6fb6ad; /* nebula teal */ --color-teal-deep: #4f8c84; --color-slate: #161b3a; /* deep indigo-slate */ --color-ink: #0b1026; /* midnight indigo */ --color-cream: #f4f1e8; /* star-white */ /* Semantic tokens (resolve per theme/section via variables). */ --color-surface: var(--surface); --color-surface-raised: var(--surface-raised); --color-surface-sunken: var(--surface-sunken); --color-text: var(--text); --color-text-muted: var(--text-muted); --color-text-subtle: var(--text-subtle); --color-border: var(--border); --color-accent: var(--accent); --color-accent-contrast: var(--accent-contrast); --color-secondary: var(--secondary); /* Typography. */ --font-sans: "Plus Jakarta Sans Variable", "Inter Variable", ui-sans-serif, system-ui, sans-serif; --font-body: "Inter Variable", ui-sans-serif, system-ui, sans-serif; --font-mono: "JetBrains Mono Variable", ui-monospace, "SFMono-Regular", monospace; /* Motion tokens (used across reveal + transition classes). */ --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1); --ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1); } /* --------------------------------------------------------------------------- * Base layer: resets, defaults, focus styles. * ------------------------------------------------------------------------- */ @layer base { :root { color-scheme: light dark; } html { scroll-behavior: smooth; /* Offset so anchor jumps clear the fixed nav. */ scroll-padding-top: 6rem; -webkit-text-size-adjust: 100%; } /* Honor reduced motion globally. */ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } } body { background-color: var(--color-surface); color: var(--color-text); font-family: var(--font-body); line-height: 1.65; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; min-height: 100dvh; } h1, h2, h3, h4 { font-family: var(--font-sans); font-weight: 700; line-height: 1.15; letter-spacing: -0.02em; color: var(--color-text); } /* Strong typographic rhythm for headings. */ h1 { font-size: clamp(2.25rem, 5vw + 1rem, 4rem); } h2 { font-size: clamp(1.75rem, 2.5vw + 1rem, 2.5rem); } h3 { font-size: clamp(1.25rem, 1vw + 1rem, 1.5rem); } a { color: var(--color-accent); text-decoration: none; transition: color 0.2s var(--ease-out-soft); } a:hover { color: var(--color-amber-soft); } /* Visible, on-brand focus ring for keyboard users. */ :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px; } ::selection { background: var(--color-amber); color: var(--color-ink); } /* Better default for images: never overflow, lazy by default in components. */ img { max-width: 100%; height: auto; } } /* --------------------------------------------------------------------------- * Component-ish utilities used across the site. Keeping them here (rather than * as repeated Tailwind class strings) makes tone adjustments easy. * ------------------------------------------------------------------------- */ @layer components { /* Content width container. */ .container-prose { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: 1.25rem; } @media (min-width: 768px) { .container-prose { padding-inline: 2rem; } } /* The recurring "card" surface. */ .card { background-color: var(--color-surface-raised); border: 1px solid var(--color-border); border-radius: 1rem; padding: 1.5rem; transition: transform 0.3s var(--ease-out-soft), box-shadow 0.3s var(--ease-out-soft), border-color 0.3s var(--ease-out-soft); } .card-interactive:hover { transform: translateY(-4px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.55); border-color: var(--color-accent); } /* Pill / tag. */ .pill { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.25rem 0.7rem; border-radius: 999px; font-size: 0.8rem; font-weight: 500; background: color-mix(in srgb, var(--color-accent) 15%, transparent); color: var(--color-text); border: 1px solid color-mix(in srgb, var(--color-accent) 35%, transparent); } /* Primary (amber) and secondary (outlined) CTA buttons. */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.75rem 1.5rem; border-radius: 0.6rem; font-family: var(--font-sans); font-weight: 600; font-size: 1rem; line-height: 1; cursor: pointer; transition: transform 0.2s var(--ease-out-soft), box-shadow 0.2s var(--ease-out-soft), background-color 0.2s var(--ease-out-soft), border-color 0.2s var(--ease-out-soft); } .btn:active { transform: translateY(1px); } .btn-primary { background: var(--color-amber); color: var(--color-ink); box-shadow: 0 10px 30px -12px rgba(245, 158, 11, 0.7); } .btn-primary:hover { background: var(--color-amber-soft); color: var(--color-ink); transform: translateY(-2px); } .btn-secondary { background: transparent; color: var(--text); border: 1.5px solid var(--color-border); } .btn-secondary:hover { border-color: var(--color-accent); color: var(--color-text); transform: translateY(-2px); } } /* Visually hidden but available to screen readers. */ .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; } /* --------------------------------------------------------------------------- * Reveal-on-scroll utility. The Reveal.astro component toggles `.is-visible`. * * The `.no-js` guard below is critical: starts with class="no-js" * (set in BaseLayout.astro) and reveal-init.ts removes it once it has wired up * the observer. If JS is disabled OR the script fails to load, that class is * never removed — so this fallback forces reveals visible rather than leaving * the page blank. This is the safety net against the original invisibility bug. * ------------------------------------------------------------------------- */ .no-js .reveal { opacity: 1 !important; transform: none !important; } /* Elegant entrance: a slow rise + soft focus-clear, with a gentle scale settle. * Slower and smoother than a plain fade-up so text "settles" into place rather * than popping. The slight blur-to-sharp adds a dreamy, mystical quality. */ .reveal { opacity: 0; transform: translateY(18px) scale(0.985); filter: blur(6px); transition: opacity 0.9s var(--ease-out-soft), transform 1s var(--ease-out-soft), filter 0.9s var(--ease-out-soft); will-change: opacity, transform, filter; } .reveal.is-visible { opacity: 1; transform: translateY(0) scale(1); filter: blur(0px); } @media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; filter: none; transition: none; } } /* --------------------------------------------------------------------------- * View Transition name utility. Assigns `view-transition-name: sky` to a single * hero/section field per page so the browser morphs the star/circuit field as * one element across navigations (constellations <-> circuitry). * * IMPORTANT: the View Transitions API requires this name to be UNIQUE within a * page snapshot. Assigning it to more than one element per page throws * "Unexpected duplicate view-transition-name: sky" and aborts the transition. * Only ONE field per page should ever carry `.vt-sky`. * * Implemented as a CSS class (not React inline style) so SSR + hydration * attributes match exactly and React never rewrites the property. * ------------------------------------------------------------------------- */ .vt-sky { view-transition-name: sky; } /* Hero expand/collapse morph. The Home hero section carries this name on Home; * the NavBar carries it on every OTHER page. So leaving Home morphs the tall * hero down into the nav bar (collapse), and arriving Home expands it back up * from the nav bar. The custom animation below makes the morph a smooth * vertical scale + position interpolation rather than the default cross-fade, * so the starry field reads as growing/shrinking rather than fading. */ .vt-hero-sky { view-transition-name: hero-sky; } ::view-transition-group(hero-sky) { animation-duration: 0.26s; animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); } ::view-transition-old(hero-sky), ::view-transition-new(hero-sky) { /* A gentle height/scale blend reads as expand/collapse rather than a swap. */ animation-duration: 0.26s; animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); overflow: clip; } /* --------------------------------------------------------------------------- * View Transitions — smooth cross-page navigation. * * Astro's uses the native View Transitions API. Without the * rules below the browser falls back to a blunt default cross-fade that feels * harsh, and worse: the NEW page is snapshotted while its `.reveal` elements * are still at opacity:0 (the scroll-reveal observer hasn't fired yet), so all * content flashes out during the morph and re-appears after the swap. The two * fixes here: * * 1. `::view-transition-group(root)` gets a short, soft duration + ease so * the swap is quick and gentle instead of slow and jarring. * 2. `html:has(...)::view-transition-*` + an `astro:before-swap` class forces * every `.reveal` to `is-visible` DURING the transition — so the snapshot * of the incoming page shows its content, eliminating the flash. (The * reveal-init.ts observer still handles the normal scroll case.) * * Reduced-motion users get an instant swap (no animation). * ------------------------------------------------------------------------- */ @view-transition { navigation: auto; } ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.18s; animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); } /* During a transition, force all reveal content visible so the incoming page's * snapshot isn't blank. `.vt-transitioning` is toggled by the script below on * astro:before-swap and removed on astro:after-swap. */ html.vt-transitioning .reveal { opacity: 1 !important; transform: none !important; filter: none !important; } @media (prefers-reduced-motion: reduce) { ::view-transition-old(root), ::view-transition-new(root), ::view-transition-group(*) { animation: none !important; } }