mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
387 lines
12 KiB
CSS
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;
|
|
}
|
|
}
|
|
|