labs / static /css /base.css
3v324v23's picture
deploy: unified router + dreamy website (2026-06-16T09:46:52Z)
c1a683f
Raw
History Blame Contribute Delete
9.57 kB
/* =========================================================================
base.css β€” reset, body sky gradient, typography defaults, utilities
========================================================================= */
/* --- Minimal reset --- */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth; /* gentle anchor jumps */
-webkit-text-size-adjust: 100%;
/* THE LOCKED SKY (paints first via inline critical <style> in <head>).
background-attachment:fixed does TWO things mobile needs:
(1) locks the gradient to the viewport so it never scrolls/gaps,
(2) is the ONLY background iOS Safari renders in the overscroll
region β€” so the rubber-band area shows pink, not bare space.
.scene (fixed, GPU-promoted) paints the same gradient on top for
the normal locked-sky view; this html layer is the bulletproof
base that loads first and covers everything. */
background: linear-gradient(
180deg,
var(--sky-top) 0%,
var(--sky-mid) 52%,
var(--sky-bottom) 100%
) fixed;
background-color: var(--sky-mid); /* solid mid-pink fallback */
overscroll-behavior-y: none;
}
body {
font-family: var(--font-body);
font-size: var(--step-body);
line-height: 1.65;
color: var(--ink);
/* The gradient now lives on <body>. This is the bulletproof layer: body
grows to the full document height and repaints synchronously with
content (it IS the content container), so a gap is physically
impossible β€” no matter how the fixed .scene/overlay layers behave.
The stretch over a tall page actually reads as the sky slowly shifting
from lavender β†’ pink β†’ peach as you descend, which suits the theme.
Transparent is GONE; body paints its own sky. */
background: linear-gradient(
180deg,
var(--sky-top) 0%,
var(--sky-mid) 52%,
var(--sky-bottom) 100%
);
background-color: var(--sky-mid); /* solid mid-pink fallback */
min-height: 100vh;
min-height: 100dvh; /* dvh = real mobile viewport (toolbar-aware) */
overflow-x: hidden; /* no horizontal scroll from floaters */
overscroll-behavior-y: none; /* no rubber-band β†’ no bare edges while scrolling */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
/* --- Headings use the rounded display face --- */
h1, h2, h3 {
font-family: var(--font-display);
font-weight: 700;
line-height: 1.12;
letter-spacing: -0.02em;
color: var(--ink);
}
p { max-width: 62ch; }
a {
color: var(--rose-deep);
text-decoration: none;
transition: color 0.2s var(--ease-gentle);
}
a:hover { color: var(--ink); }
/* Soft text shadow so text stays dreamy-readable on any cloud area */
.hero h1,
.hero p,
.brand,
.manifesto h2,
.manifesto p {
text-shadow: 0 2px 24px rgba(255, 214, 232, 0.55),
0 1px 2px rgba(255, 255, 255, 0.4);
}
/* --- Layout helpers --- */
.container {
width: 100%;
max-width: var(--maxw);
margin-inline: auto;
padding-inline: var(--gut);
}
.center { text-align: center; }
.muted { color: var(--ink-soft); }
/* --- Buttons --- */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5em;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.0625rem;
letter-spacing: 0.01em;
padding: 0.95em 1.7em;
border-radius: var(--radius-pill);
border: none;
cursor: pointer;
transition: transform 0.2s var(--ease-soft),
box-shadow 0.25s var(--ease-soft),
background 0.2s var(--ease-gentle);
text-decoration: none;
}
.btn-primary {
background: var(--rose);
color: #fff;
box-shadow: 0 10px 28px rgba(255, 143, 171, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.btn-primary:hover {
background: var(--rose-deep);
color: #fff;
box-shadow: 0 14px 36px rgba(232, 111, 146, 0.55),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.btn-primary:active { transform: scale(0.97); }
.btn-ghost {
background: rgba(255, 255, 255, 0.55);
color: var(--ink);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1.5px solid rgba(255, 143, 171, 0.3);
}
.btn-ghost:hover {
background: rgba(255, 255, 255, 0.78);
color: var(--ink);
}
.btn-ghost:active { transform: scale(0.97); }
/* Visible focus ring for keyboard users */
.btn:focus-visible,
a:focus-visible {
outline: 3px solid var(--rose-deep);
outline-offset: 3px;
}
/* --- Selection --- */
::selection {
background: var(--rose);
color: #fff;
}
/* --- Reduced motion: kill all ambient drift globally --- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
/* =========================================================================
Full-site dream overlays
Order (back β†’ front): content (0–3) β†’ BLUR overlay (4) β†’ GRAIN overlay (5)
Both are pointer-events: none so they never block interaction.
========================================================================= */
/* Layer A β€” soft blur over the whole site. Sits ABOVE everything except
the grain, giving the page a hazy, dreamy soft-focus feel. */
.blur-overlay {
position: fixed;
top: -16px; /* overscan: extend beyond viewport on all sides */
right: -16px; /* so compositor lag / toolbar transitions never */
bottom: -16px; /* reveal a gap strip at any edge during scroll */
left: -16px;
z-index: 4;
pointer-events: none;
transform: translateZ(0);
will-change: transform;
backdrop-filter: blur(0.5px); /* subtle dreamy blur */
-webkit-backdrop-filter: blur(0.5px);
}
/* Layer B β€” animated film grain ON TOP of the blur. SVG fractal noise as
a data URI, tiled and jittered with steps() so it shimmers like real
analog grain. mix-blend-mode keeps it tonal, not grey. */
.grain-overlay {
position: fixed;
top: -16px; /* overscan (same reason as .blur-overlay) */
right: -16px;
bottom: -16px;
left: -16px;
z-index: 5;
pointer-events: none;
opacity: 0.09; /* keep it whisper-light */
mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 220px 220px;
animation: grain 0.6s steps(3) infinite;
}
@keyframes grain {
0% { transform: translate(0, 0); }
33% { transform: translate(-4px, 3px); }
66% { transform: translate(3px, -4px); }
100% { transform: translate(-2px, -2px); }
}
@media (prefers-reduced-motion: reduce) {
.grain-overlay { animation: none; }
}
/* On touch devices the dreamy blur + grain are now RESTORED β€” the real
cause of the scroll strip (a fixed .scene gradient mismatched against a
scrolling body gradient) is fixed, so a frozen overlay layer no longer
reveals a mismatched colour underneath. 0.5px blur is subtle enough that
even a momentary freeze reads as nothing, and the grain is noise (never
colour-mismatched). Desktop keeps its defaults via the rules above. */
@media (hover: none), (pointer: coarse) {
/* grain boosted slightly so it reads on bright mobile screens */
.grain-overlay { opacity: 0.12; }
}
/* =========================================================================
Motion system β€” word-by-word split + scroll reveals
Driven by js/motion.js. transform/opacity only, GPU-safe.
========================================================================= */
/* word-by-word split: each word starts clipped + offset, eases in */
.word {
display: inline-block;
opacity: 0;
transform: translateY(0.5em) rotate(2deg);
filter: blur(6px);
transition: opacity var(--dur-slow) var(--ease-soft),
transform var(--dur-slow) var(--ease-spring),
filter var(--dur-slow) var(--ease-soft);
will-change: transform, opacity, filter;
}
.word.is-in {
opacity: 1;
transform: translateY(0) rotate(0);
filter: blur(0);
}
/* generic scroll reveal: fade + rise */
[data-reveal],
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity var(--dur-slow) var(--ease-soft),
transform var(--dur-slow) var(--ease-soft);
will-change: transform, opacity;
}
[data-reveal].is-in,
.reveal.is-in {
opacity: 1;
transform: translateY(0);
}
/* stagger helpers (keep parity with the old .dN classes) */
[data-reveal].d1, .reveal.d1 { transition-delay: 0.08s; }
[data-reveal].d2, .reveal.d2 { transition-delay: 0.16s; }
[data-reveal].d3, .reveal.d3 { transition-delay: 0.24s; }
[data-reveal].d4, .reveal.d4 { transition-delay: 0.32s; }
/* pinned constellation: the section sticks while the stage scrubs in */
.constellation-pin {
position: relative;
}
.constellation-stage {
transition: opacity var(--dur-cinema) var(--ease-flow),
transform var(--dur-cinema) var(--ease-flow);
}
@media (prefers-reduced-motion: reduce) {
.word { opacity: 1; transform: none; filter: none; }
[data-reveal], .reveal { opacity: 1; transform: none; }
}