Files
com.gregkilmer.www/src/styles/global.css
T
2026-07-04 18:03:38 -04:00

387 lines
12 KiB
CSS

/**
* 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: <html> 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 <ClientRouter /> 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;
}
}