Add Singularity template: WebGL nebula + holographic cards
Third theme template scoped to :root[data-template="cosmos"], joining editorial and swiss. Set "theme.template": "cosmos" in links.json to activate. Background — full-viewport WebGL fragment shader running a 5-octave warped fBm nebula in indigo/violet/magenta/cyan, with a two-layer procedural starfield (small dense + sparse blue-white halo) twinkling on independent phases. Pointer-tracking gravity well subtly perturbs sampling around the cursor. Pixel ratio capped at 1.5x for fill-rate; loop pauses on visibility change. UI — Orbitron display + Geist body, glassmorphic cards with backdrop-filter, conic-gradient holographic borders driven by @property --cosmos-hue rotating on hover, gradient-clip section numbers and project titles, chromatic-split hero last name on hover (::before + ::after pseudos with mix-blend-mode screen using a data-text attr), glowing drift-orb in place of the editorial asterism, animated section-head accent line. Fallbacks — prefers-reduced-motion or WebGL-unavailable triggers the .cosmos-static class with a CSS-only radial-gradient nebula and all heavy animations disabled. Always-dark regardless of theme toggle. README documents the new template; links.example.json unchanged.
This commit is contained in:
25
README.md
25
README.md
@@ -81,7 +81,7 @@ asset change.
|
||||
| field | type | values | description |
|
||||
|------------|--------|---------------------------------|-------------|
|
||||
| `accent` | string | hex color, e.g. `"#E8482C"` | Single sharp accent color used throughout. |
|
||||
| `template` | string | `"editorial"` (default) or `"swiss"` | Visual treatment. See [Templates](#templates). |
|
||||
| `template` | string | `"editorial"` (default), `"swiss"`, or `"cosmos"` | Visual treatment. See [Templates](#templates). |
|
||||
| `mode` | string | unused; kept for future use | Theme toggle (auto/light/dark) is controlled by the corner pill, not this field. |
|
||||
|
||||
### `sections`
|
||||
@@ -366,12 +366,35 @@ Müller-Brockmann tribute. Akzidenz/Univers spirit with [Archivo](https://fonts.
|
||||
- **Cards:** invert on hover (white → solid black with paper text). Featured cards are solid red, invert to black on hover.
|
||||
- **Animations:** no spin, no italic — only sharp pop-in for the geometric mark
|
||||
|
||||
### `"cosmos"`
|
||||
|
||||
Interstellar / sci-fi treatment. Lit by a real WebGL fragment shader
|
||||
running in a fixed full-viewport canvas behind the page.
|
||||
|
||||
- **Type:** Orbitron (variable sans display) + Geist for body
|
||||
- **Mood:** deep indigo paper, cyan + magenta + violet accents, dark-only
|
||||
(the light/auto theme toggle is overridden — cosmos is always night)
|
||||
- **Background:** animated fractal-noise nebula with a procedural
|
||||
two-layer starfield, subtle "gravity well" perturbation toward the
|
||||
cursor, ~150 LOC GLSL fragment shader
|
||||
- **Cards:** glassmorphic with `backdrop-filter`, conic-gradient
|
||||
holographic borders that rotate on hover (`@property --cosmos-hue`)
|
||||
- **Hero:** glowing chromatic-split last name on hover, drifting
|
||||
light-orb in place of the asterism, gradient-clip wordmarks
|
||||
- **Fallbacks:** when WebGL is unavailable or
|
||||
`prefers-reduced-motion: reduce` is set, a static CSS-only radial
|
||||
nebula renders instead — and all heavy animations are disabled
|
||||
|
||||
To switch:
|
||||
|
||||
```json
|
||||
"theme": { "accent": "#E8482C", "template": "swiss" }
|
||||
```
|
||||
|
||||
```json
|
||||
"theme": { "template": "cosmos" }
|
||||
```
|
||||
|
||||
Choice is also cached in `localStorage` (`dlstack-template`) so reloads
|
||||
don't flash the wrong template before JSON parses.
|
||||
|
||||
|
||||
595
assets/css/cosmos.css
Normal file
595
assets/css/cosmos.css
Normal file
@@ -0,0 +1,595 @@
|
||||
/* dlstack — Singularity template
|
||||
WebGL nebula + procedural starfield + chromatic-glow hero.
|
||||
Scoped to :root[data-template="cosmos"] so it's inert otherwise.
|
||||
*/
|
||||
|
||||
@property --cosmos-hue {
|
||||
syntax: "<angle>";
|
||||
inherits: false;
|
||||
initial-value: 0deg;
|
||||
}
|
||||
|
||||
:root[data-template="cosmos"] {
|
||||
--paper: #050211;
|
||||
--paper-2: #0E0826;
|
||||
--ink: #ECF1FF;
|
||||
--ink-2: #C2CDF0;
|
||||
--muted: #8693C2;
|
||||
--rule: rgba(180, 200, 255, 0.16);
|
||||
--accent: #7AF7FF;
|
||||
--accent-2: #FF4ECD;
|
||||
--accent-3: #B07AFF;
|
||||
--on-accent: #050211;
|
||||
--display: "Orbitron", "Geist", system-ui, sans-serif;
|
||||
--body: "Geist", system-ui, sans-serif;
|
||||
--mono: "Geist Mono", ui-monospace, Menlo, monospace;
|
||||
--radius: 14px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* always dark — ignore data-theme on cosmos */
|
||||
:root[data-template="cosmos"][data-theme="light"],
|
||||
:root[data-template="cosmos"][data-theme="dark"],
|
||||
:root[data-template="cosmos"][data-theme="auto"] {
|
||||
--paper: #050211;
|
||||
--paper-2: #0E0826;
|
||||
--ink: #ECF1FF;
|
||||
--ink-2: #C2CDF0;
|
||||
--muted: #8693C2;
|
||||
}
|
||||
|
||||
/* remove editorial paper grain */
|
||||
:root[data-template="cosmos"] body::before { display: none; }
|
||||
|
||||
/* shader canvas — fixed behind everything */
|
||||
:root[data-template="cosmos"] .cosmos-bg {
|
||||
position: fixed; inset: 0;
|
||||
z-index: 0;
|
||||
width: 100vw; height: 100vh;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: cosmos-fade-in 1800ms 200ms cubic-bezier(0.16, 1, 0.30, 1) forwards;
|
||||
}
|
||||
@keyframes cosmos-fade-in { to { opacity: 1; } }
|
||||
|
||||
/* CSS-only static fallback nebula when WebGL is unavailable */
|
||||
:root[data-template="cosmos"].cosmos-static body {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(176, 122, 255, 0.35), transparent 55%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(255, 78, 205, 0.28), transparent 50%),
|
||||
radial-gradient(ellipse at 50% 100%, rgba(122, 247, 255, 0.22), transparent 55%),
|
||||
var(--paper);
|
||||
}
|
||||
:root[data-template="cosmos"].cosmos-static body::after {
|
||||
content: ""; position: fixed; inset: 0; pointer-events: none;
|
||||
background-image:
|
||||
radial-gradient(1px 1px at 12% 22%, #fff 50%, transparent 100%),
|
||||
radial-gradient(1px 1px at 78% 33%, #fff 50%, transparent 100%),
|
||||
radial-gradient(1.5px 1.5px at 44% 78%, #fff 50%, transparent 100%),
|
||||
radial-gradient(1px 1px at 24% 88%, #fff 50%, transparent 100%),
|
||||
radial-gradient(1px 1px at 64% 14%, #fff 50%, transparent 100%),
|
||||
radial-gradient(1px 1px at 90% 90%, #fff 50%, transparent 100%);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
:root[data-template="cosmos"] body {
|
||||
background: var(--paper);
|
||||
}
|
||||
:root[data-template="cosmos"] .shell { z-index: 1; }
|
||||
|
||||
/* ───── marker bar ───── */
|
||||
:root[data-template="cosmos"] .marker {
|
||||
border-bottom: 1px solid var(--rule);
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
:root[data-template="cosmos"] .marker__brand { color: var(--accent); gap: 0.55rem; }
|
||||
:root[data-template="cosmos"] .marker__brand .star {
|
||||
width: 10px; height: 10px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
font-size: 0; transform: none;
|
||||
box-shadow:
|
||||
0 0 8px var(--accent),
|
||||
0 0 18px color-mix(in oklch, var(--accent) 80%, transparent),
|
||||
0 0 32px color-mix(in oklch, var(--accent-2) 60%, transparent);
|
||||
animation: cosmos-pulse 2.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes cosmos-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.3); opacity: 0.75; }
|
||||
}
|
||||
:root[data-template="cosmos"] .marker__year {
|
||||
color: var(--accent-2);
|
||||
text-shadow: 0 0 12px color-mix(in oklch, var(--accent-2) 60%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .theme {
|
||||
border-radius: 999px;
|
||||
border-color: var(--rule);
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink-2);
|
||||
background: rgba(20, 14, 50, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
:root[data-template="cosmos"] .theme:hover {
|
||||
background: rgba(122, 247, 255, 0.12);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 0 18px color-mix(in oklch, var(--accent) 40%, transparent);
|
||||
}
|
||||
|
||||
/* ───── hero ───── */
|
||||
:root[data-template="cosmos"] .hero { position: relative; }
|
||||
:root[data-template="cosmos"] .hero__name {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-variation-settings: normal;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 0.92;
|
||||
color: var(--ink);
|
||||
position: relative;
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name .hero__name-first {
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
text-shadow:
|
||||
0 0 24px rgba(122, 247, 255, 0.45),
|
||||
0 0 60px rgba(176, 122, 255, 0.30);
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name em {
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
color: var(--accent-2);
|
||||
text-shadow:
|
||||
0 0 24px rgba(255, 78, 205, 0.55),
|
||||
0 0 50px rgba(255, 78, 205, 0.35),
|
||||
0 0 90px rgba(176, 122, 255, 0.30);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name em::before,
|
||||
:root[data-template="cosmos"] .hero__name em::after {
|
||||
content: attr(data-text);
|
||||
position: absolute; left: 0; top: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name em::before {
|
||||
color: var(--accent);
|
||||
transform: translate(-3px, 0);
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name em::after {
|
||||
color: var(--accent-2);
|
||||
transform: translate(3px, 0);
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name:hover em::before,
|
||||
:root[data-template="cosmos"] .hero__name:hover em::after {
|
||||
opacity: 0.7;
|
||||
animation: cosmos-rgb-shift 1.2s steps(8) infinite;
|
||||
}
|
||||
@keyframes cosmos-rgb-shift {
|
||||
0% { transform: translate(-3px, 0); }
|
||||
25% { transform: translate(-2px, 1px); }
|
||||
50% { transform: translate(-4px, -1px); }
|
||||
75% { transform: translate(-2px, 0); }
|
||||
100% { transform: translate(-3px, 0); }
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__name:hover em::after {
|
||||
animation-name: cosmos-rgb-shift-2;
|
||||
}
|
||||
@keyframes cosmos-rgb-shift-2 {
|
||||
0% { transform: translate(3px, 0); }
|
||||
25% { transform: translate(4px, -1px); }
|
||||
50% { transform: translate(2px, 1px); }
|
||||
75% { transform: translate(4px, 0); }
|
||||
100% { transform: translate(3px, 0); }
|
||||
}
|
||||
|
||||
/* asterism becomes a glowing orb */
|
||||
:root[data-template="cosmos"] .hero__asterism {
|
||||
font-size: 0; color: transparent;
|
||||
width: clamp(4rem, 8vw, 6.5rem);
|
||||
height: clamp(4rem, 8vw, 6.5rem);
|
||||
background: radial-gradient(circle at 35% 35%,
|
||||
#fff 0%,
|
||||
var(--accent) 25%,
|
||||
var(--accent-3) 55%,
|
||||
transparent 80%);
|
||||
border-radius: 50%;
|
||||
transform: none;
|
||||
filter: blur(0.5px);
|
||||
box-shadow:
|
||||
0 0 40px var(--accent),
|
||||
0 0 80px var(--accent-3),
|
||||
0 0 120px color-mix(in oklch, var(--accent-2) 60%, transparent);
|
||||
animation:
|
||||
cosmos-orb-rise 1400ms 300ms cubic-bezier(0.16, 1, 0.30, 1) both,
|
||||
cosmos-orb-drift 9s ease-in-out 1700ms infinite;
|
||||
}
|
||||
@keyframes cosmos-orb-rise {
|
||||
from { opacity: 0; transform: scale(0.2) translateY(-20px); }
|
||||
to { opacity: 0.95; transform: scale(1) translateY(0); }
|
||||
}
|
||||
@keyframes cosmos-orb-drift {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
33% { transform: translate(-8px, 6px); }
|
||||
66% { transform: translate(6px, -4px); }
|
||||
}
|
||||
|
||||
:root[data-template="cosmos"] .hero__tagline {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variation-settings: normal;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ink-2);
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(1rem, 0.8rem + 0.9vw, 1.4rem);
|
||||
max-width: 36ch;
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__tagline span {
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 16px color-mix(in oklch, var(--accent) 70%, transparent);
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
:root[data-template="cosmos"] .hero__bio {
|
||||
color: var(--ink-2);
|
||||
font-family: var(--body);
|
||||
}
|
||||
|
||||
/* ───── social ───── */
|
||||
:root[data-template="cosmos"] .social a {
|
||||
border-radius: 999px;
|
||||
border-color: var(--rule);
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--fs-mini);
|
||||
padding: 10px 16px;
|
||||
color: var(--ink-2);
|
||||
background: rgba(20, 14, 50, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
:root[data-template="cosmos"] .social a:hover {
|
||||
background: rgba(122, 247, 255, 0.08);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 0 22px color-mix(in oklch, var(--accent) 35%, transparent),
|
||||
0 8px 22px -10px color-mix(in oklch, var(--accent) 50%, transparent);
|
||||
}
|
||||
|
||||
/* ───── sections ───── */
|
||||
:root[data-template="cosmos"] .section__head {
|
||||
border-bottom: 1px solid var(--rule);
|
||||
position: relative;
|
||||
}
|
||||
:root[data-template="cosmos"] .section__head::after {
|
||||
content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
var(--accent) 0%,
|
||||
var(--accent-3) 40%,
|
||||
var(--accent-2) 70%,
|
||||
transparent 100%);
|
||||
box-shadow: 0 0 12px var(--accent-3);
|
||||
transform-origin: left;
|
||||
animation: cosmos-line-in 1400ms cubic-bezier(0.16, 1, 0.30, 1) both;
|
||||
}
|
||||
@keyframes cosmos-line-in {
|
||||
from { transform: scaleX(0); opacity: 0; }
|
||||
to { transform: scaleX(1); opacity: 1; }
|
||||
}
|
||||
:root[data-template="cosmos"] .section__numwrap small {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-family: var(--display);
|
||||
font-variation-settings: normal;
|
||||
letter-spacing: 0.22em;
|
||||
font-size: var(--fs-sm);
|
||||
text-shadow: 0 0 12px color-mix(in oklch, var(--accent) 60%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .section__num {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.04em;
|
||||
background: linear-gradient(180deg, var(--ink) 0%, var(--accent) 60%, var(--accent-3) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
filter: drop-shadow(0 0 18px rgba(122, 247, 255, 0.4));
|
||||
}
|
||||
:root[data-template="cosmos"] .section__title {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-variation-settings: normal;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="cosmos"] .section__kicker {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ───── cards — holographic ───── */
|
||||
:root[data-template="cosmos"] .card {
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--rule);
|
||||
background: linear-gradient(135deg, rgba(30, 22, 64, 0.55), rgba(14, 8, 38, 0.55));
|
||||
backdrop-filter: blur(14px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(140%);
|
||||
color: var(--ink);
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
}
|
||||
:root[data-template="cosmos"] .card::before {
|
||||
content: ""; position: absolute; inset: -1px;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: conic-gradient(from var(--cosmos-hue, 0deg),
|
||||
var(--accent), var(--accent-2), var(--accent-3), var(--accent));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 350ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
:root[data-template="cosmos"] .card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: transparent;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.04),
|
||||
0 18px 44px -18px rgba(122, 247, 255, 0.35),
|
||||
0 6px 20px -8px rgba(255, 78, 205, 0.30);
|
||||
}
|
||||
:root[data-template="cosmos"] .card:hover::before {
|
||||
opacity: 1;
|
||||
animation: cosmos-hue-rot 6s linear infinite;
|
||||
}
|
||||
@keyframes cosmos-hue-rot {
|
||||
to { --cosmos-hue: 360deg; }
|
||||
}
|
||||
:root[data-template="cosmos"] .card::after {
|
||||
color: var(--accent);
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-shadow: 0 0 8px color-mix(in oklch, var(--accent) 70%, transparent);
|
||||
}
|
||||
|
||||
:root[data-template="cosmos"] .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="cosmos"] .card__desc {
|
||||
font-family: var(--body);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
:root[data-template="cosmos"] .card__host {
|
||||
font-family: var(--display);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 8px color-mix(in oklch, var(--accent) 50%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .card__date {
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.10em;
|
||||
color: var(--muted);
|
||||
}
|
||||
:root[data-template="cosmos"] .card__meta .card__date::before { color: var(--accent-2); }
|
||||
|
||||
/* link favicon */
|
||||
:root[data-template="cosmos"] .card--link .favicon {
|
||||
border-radius: 10px;
|
||||
border-color: var(--rule);
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
/* project card */
|
||||
:root[data-template="cosmos"] .card--project .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
font-size: var(--fs-lg);
|
||||
line-height: 1.05;
|
||||
background: linear-gradient(135deg, var(--ink) 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* featured — accent glow card */
|
||||
:root[data-template="cosmos"] .card--featured {
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(255, 78, 205, 0.45), transparent 60%),
|
||||
radial-gradient(circle at 0% 100%, rgba(122, 247, 255, 0.4), transparent 65%),
|
||||
linear-gradient(135deg, rgba(176, 122, 255, 0.45), rgba(14, 8, 38, 0.8));
|
||||
border-color: rgba(255,255,255,0.12);
|
||||
color: #fff;
|
||||
}
|
||||
:root[data-template="cosmos"] .card--featured::before { opacity: 0.55; animation: cosmos-hue-rot 6s linear infinite; }
|
||||
:root[data-template="cosmos"] .card--featured:hover::before { opacity: 1; }
|
||||
:root[data-template="cosmos"] .card--featured .card__title {
|
||||
-webkit-text-fill-color: #fff;
|
||||
background: none;
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 0 24px rgba(122, 247, 255, 0.55),
|
||||
0 0 50px rgba(255, 78, 205, 0.45);
|
||||
font-weight: 700;
|
||||
font-size: clamp(1.85rem, 1.30rem + 1.6vw, 2.6rem);
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
:root[data-template="cosmos"] .card--featured .card__desc { color: rgba(255,255,255,0.85); }
|
||||
:root[data-template="cosmos"] .card--featured::after { color: #fff; }
|
||||
|
||||
/* tags */
|
||||
:root[data-template="cosmos"] .tag {
|
||||
border-radius: 999px;
|
||||
border-color: rgba(180, 200, 255, 0.22);
|
||||
background: rgba(122, 247, 255, 0.08);
|
||||
color: var(--accent);
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.10em;
|
||||
}
|
||||
:root[data-template="cosmos"] .card:hover .tag {
|
||||
border-color: var(--accent);
|
||||
background: rgba(122, 247, 255, 0.16);
|
||||
}
|
||||
:root[data-template="cosmos"] .card--featured .tag {
|
||||
background: rgba(255,255,255,0.14);
|
||||
border-color: rgba(255,255,255,0.30);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* youtube */
|
||||
:root[data-template="cosmos"] .card--youtube { border-color: var(--rule); }
|
||||
:root[data-template="cosmos"] .yt__play {
|
||||
background: rgba(5, 2, 17, 0.5);
|
||||
border: 1.5px solid var(--accent);
|
||||
box-shadow:
|
||||
0 0 18px color-mix(in oklch, var(--accent) 50%, transparent),
|
||||
inset 0 0 12px color-mix(in oklch, var(--accent) 30%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .yt__play svg { fill: var(--accent); }
|
||||
:root[data-template="cosmos"] .yt:hover .yt__play {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 0 30px var(--accent),
|
||||
0 0 60px var(--accent-3);
|
||||
}
|
||||
:root[data-template="cosmos"] .yt:hover .yt__play svg { fill: var(--paper); }
|
||||
:root[data-template="cosmos"] .yt__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-shadow: 0 0 12px rgba(122, 247, 255, 0.5);
|
||||
}
|
||||
|
||||
/* portfolio */
|
||||
:root[data-template="cosmos"] .card--portfolio { border-color: var(--rule); }
|
||||
:root[data-template="cosmos"] .portfolio__caption {
|
||||
border-top: 1px solid var(--rule);
|
||||
background: rgba(14, 8, 38, 0.55);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
:root[data-template="cosmos"] .portfolio__caption .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.005em;
|
||||
background: linear-gradient(135deg, var(--ink) 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
:root[data-template="cosmos"] .card--portfolio:hover .portfolio__caption .card__title {
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* clients */
|
||||
:root[data-template="cosmos"] .client__logo {
|
||||
border-radius: 14px;
|
||||
border-color: var(--rule);
|
||||
background: rgba(255,255,255,0.03);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
:root[data-template="cosmos"] .card--client:hover .client__logo {
|
||||
border-color: var(--accent);
|
||||
background: rgba(122, 247, 255, 0.08);
|
||||
box-shadow:
|
||||
0 0 24px color-mix(in oklch, var(--accent) 30%, transparent),
|
||||
0 8px 22px -10px color-mix(in oklch, var(--accent) 40%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .client__logo[data-fallback] {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 14px color-mix(in oklch, var(--accent) 60%, transparent);
|
||||
}
|
||||
:root[data-template="cosmos"] .client__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--ink-2);
|
||||
}
|
||||
:root[data-template="cosmos"] .card--client:hover .client__title { color: var(--accent); }
|
||||
|
||||
/* footer */
|
||||
:root[data-template="cosmos"] .foot {
|
||||
border-top: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
font-family: var(--display);
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
:root[data-template="cosmos"] .foot__mark {
|
||||
font-family: var(--display);
|
||||
font-weight: 900;
|
||||
color: var(--accent);
|
||||
text-shadow: 0 0 16px color-mix(in oklch, var(--accent) 60%, transparent);
|
||||
}
|
||||
|
||||
/* selection */
|
||||
:root[data-template="cosmos"] ::selection {
|
||||
background: var(--accent);
|
||||
color: var(--paper);
|
||||
}
|
||||
:root[data-template="cosmos"] :focus-visible { outline-color: var(--accent); }
|
||||
|
||||
/* reduced motion — strip glows + animations */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root[data-template="cosmos"] .marker__brand .star,
|
||||
:root[data-template="cosmos"] .hero__asterism,
|
||||
:root[data-template="cosmos"] .section__head::after,
|
||||
:root[data-template="cosmos"] .card::before {
|
||||
animation: none !important;
|
||||
}
|
||||
:root[data-template="cosmos"] .cosmos-bg { display: none; }
|
||||
:root[data-template="cosmos"].cosmos-static body {
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 20%, rgba(176, 122, 255, 0.25), transparent 60%),
|
||||
radial-gradient(ellipse at 70% 80%, rgba(122, 247, 255, 0.18), transparent 60%),
|
||||
var(--paper);
|
||||
}
|
||||
}
|
||||
182
assets/js/app.js
182
assets/js/app.js
@@ -160,7 +160,7 @@
|
||||
const parts = String(name).trim().split(/\s+/);
|
||||
if (parts.length < 2) return `<span class="hero__name-first">${esc(name)}</span>`;
|
||||
const last = parts.pop();
|
||||
return `<span class="hero__name-first">${esc(parts.join(" "))}</span> <em>${esc(last)}</em>`;
|
||||
return `<span class="hero__name-first">${esc(parts.join(" "))}</span> <em data-text="${esc(last)}">${esc(last)}</em>`;
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
@@ -235,6 +235,182 @@
|
||||
});
|
||||
}
|
||||
|
||||
function bootCosmos() {
|
||||
const root = document.documentElement;
|
||||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
if (reducedMotion) { root.classList.add("cosmos-static"); return; }
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.className = "cosmos-bg";
|
||||
canvas.setAttribute("aria-hidden", "true");
|
||||
const gl = canvas.getContext("webgl", { antialias: false, alpha: false, premultipliedAlpha: false }) ||
|
||||
canvas.getContext("experimental-webgl");
|
||||
if (!gl) { root.classList.add("cosmos-static"); return; }
|
||||
|
||||
const vsSrc = `
|
||||
attribute vec2 a;
|
||||
void main() { gl_Position = vec4(a, 0.0, 1.0); }
|
||||
`;
|
||||
const fsSrc = `
|
||||
precision highp float;
|
||||
uniform vec2 u_res;
|
||||
uniform float u_time;
|
||||
uniform vec2 u_mouse;
|
||||
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p), f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
|
||||
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
|
||||
}
|
||||
float fbm(vec2 p) {
|
||||
float v = 0.0, a = 0.55;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
v += a * noise(p);
|
||||
p *= 2.04; a *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 frag = gl_FragCoord.xy;
|
||||
vec2 uv = frag / u_res;
|
||||
vec2 p = (frag - 0.5 * u_res) / u_res.y;
|
||||
|
||||
// gravity well: pull sampling toward cursor very subtly
|
||||
vec2 m = (u_mouse - 0.5 * u_res) / u_res.y;
|
||||
vec2 toM = p - m;
|
||||
float md = length(toM) + 0.001;
|
||||
p -= (toM / md) * 0.03 * exp(-md * 1.6);
|
||||
|
||||
// nebula via warped fbm
|
||||
vec2 q = p * 1.25 + vec2(u_time * 0.015, u_time * 0.010);
|
||||
vec2 r = q + vec2(fbm(q + u_time * 0.04), fbm(q - u_time * 0.03));
|
||||
float n = fbm(r);
|
||||
|
||||
// palette: deep indigo -> magenta -> violet -> cyan ridges
|
||||
vec3 deep = vec3(0.020, 0.012, 0.055);
|
||||
vec3 violet = vec3(0.350, 0.150, 0.620);
|
||||
vec3 magenta = vec3(0.950, 0.220, 0.700);
|
||||
vec3 cyan = vec3(0.290, 0.940, 1.000);
|
||||
|
||||
vec3 col = deep;
|
||||
col = mix(col, violet, smoothstep(0.25, 0.70, n));
|
||||
col = mix(col, magenta, smoothstep(0.55, 0.92, n) * 0.85);
|
||||
col += cyan * smoothstep(0.78, 0.98, n) * 0.45;
|
||||
|
||||
// soft vignette
|
||||
float vig = smoothstep(1.25, 0.30, length(p));
|
||||
col *= mix(0.55, 1.05, vig);
|
||||
|
||||
// ───── starfield — two layers ─────
|
||||
// layer 1 — small dense
|
||||
float starDensity = 90.0;
|
||||
vec2 sc = uv * starDensity * vec2(u_res.x / u_res.y, 1.0);
|
||||
vec2 si = floor(sc);
|
||||
vec2 sf = fract(sc) - 0.5;
|
||||
float sh = hash(si);
|
||||
if (sh > 0.986) {
|
||||
float d = length(sf);
|
||||
float tw = 0.55 + 0.45 * sin(u_time * 2.4 + sh * 88.0);
|
||||
col += vec3(smoothstep(0.05, 0.0, d) * tw);
|
||||
}
|
||||
// layer 2 — bright sparse blue-white
|
||||
starDensity = 28.0;
|
||||
sc = uv * starDensity * vec2(u_res.x / u_res.y, 1.0);
|
||||
si = floor(sc);
|
||||
sf = fract(sc) - 0.5;
|
||||
sh = hash(si + 13.7);
|
||||
if (sh > 0.993) {
|
||||
float d = length(sf);
|
||||
float tw = 0.3 + 0.7 * sin(u_time * 1.7 + sh * 200.0);
|
||||
float s = smoothstep(0.09, 0.0, d) * tw;
|
||||
col += vec3(0.72, 0.86, 1.0) * s * 1.8;
|
||||
// soft halo
|
||||
col += vec3(0.45, 0.55, 0.95) * smoothstep(0.28, 0.0, d) * tw * 0.18;
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const compile = (type, src) => {
|
||||
const s = gl.createShader(type);
|
||||
gl.shaderSource(s, src); gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||||
console.warn("[cosmos]", gl.getShaderInfoLog(s));
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
const vs = compile(gl.VERTEX_SHADER, vsSrc);
|
||||
const fs = compile(gl.FRAGMENT_SHADER, fsSrc);
|
||||
if (!vs || !fs) { root.classList.add("cosmos-static"); return; }
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, vs); gl.attachShader(prog, fs);
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
||||
console.warn("[cosmos]", gl.getProgramInfoLog(prog));
|
||||
root.classList.add("cosmos-static"); return;
|
||||
}
|
||||
gl.useProgram(prog);
|
||||
|
||||
const buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
-1,-1, 1,-1, -1, 1,
|
||||
-1, 1, 1,-1, 1, 1
|
||||
]), gl.STATIC_DRAW);
|
||||
const aLoc = gl.getAttribLocation(prog, "a");
|
||||
gl.enableVertexAttribArray(aLoc);
|
||||
gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
const uRes = gl.getUniformLocation(prog, "u_res");
|
||||
const uTime = gl.getUniformLocation(prog, "u_time");
|
||||
const uMouse = gl.getUniformLocation(prog, "u_mouse");
|
||||
|
||||
document.body.prepend(canvas);
|
||||
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||||
let w = 0, h = 0;
|
||||
const resize = () => {
|
||||
w = Math.floor(window.innerWidth * dpr);
|
||||
h = Math.floor(window.innerHeight * dpr);
|
||||
canvas.width = w; canvas.height = h;
|
||||
gl.viewport(0, 0, w, h);
|
||||
};
|
||||
resize();
|
||||
window.addEventListener("resize", resize, { passive: true });
|
||||
|
||||
const mouse = { x: 0.5, y: 0.5 };
|
||||
const target = { x: 0.5, y: 0.5 };
|
||||
window.addEventListener("pointermove", (e) => {
|
||||
target.x = e.clientX / window.innerWidth;
|
||||
target.y = 1 - e.clientY / window.innerHeight;
|
||||
}, { passive: true });
|
||||
|
||||
let running = true;
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
const wasPaused = !running;
|
||||
running = !document.hidden;
|
||||
if (running && wasPaused) requestAnimationFrame(loop);
|
||||
});
|
||||
|
||||
const t0 = performance.now();
|
||||
function loop(now) {
|
||||
if (!running) return;
|
||||
mouse.x += (target.x - mouse.x) * 0.05;
|
||||
mouse.y += (target.y - mouse.y) * 0.05;
|
||||
gl.uniform2f(uRes, w, h);
|
||||
gl.uniform1f(uTime, (now - t0) / 1000);
|
||||
gl.uniform2f(uMouse, mouse.x * w, mouse.y * h);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function attachReveal(root) {
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
$$(".reveal", root).forEach(el => el.classList.add("in"));
|
||||
@@ -277,9 +453,11 @@
|
||||
if (data.theme?.accent) {
|
||||
document.documentElement.style.setProperty("--accent", data.theme.accent);
|
||||
}
|
||||
const tpl = data.theme?.template === "swiss" ? "swiss" : "editorial";
|
||||
const validTpl = new Set(["editorial", "swiss", "cosmos"]);
|
||||
const tpl = validTpl.has(data.theme?.template) ? data.theme.template : "editorial";
|
||||
document.documentElement.dataset.template = tpl;
|
||||
try { localStorage.setItem("dlstack-template", tpl); } catch (e) {}
|
||||
if (tpl === "cosmos") bootCosmos();
|
||||
|
||||
const p = data.profile || {};
|
||||
const sections = data.sections || [];
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<!-- Editorial: Fraunces (variable serif) + Geist (refined sans) + Geist Mono
|
||||
Swiss: Archivo (variable grotesque, near-Akzidenz/Univers) -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;900&family=DM+Serif+Display:ital@0;1&family=Fraunces:opsz,wght,SOFT,WONK@9..144,300..600,30..100,0..1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;900&family=DM+Serif+Display:ital@0;1&family=Fraunces:opsz,wght,SOFT,WONK@9..144,300..600,30..100,0..1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&family=Orbitron:wght@400;500;700;900&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
// Apply saved theme + template before paint to avoid flash
|
||||
@@ -27,12 +27,13 @@
|
||||
var t = localStorage.getItem("dlstack-theme");
|
||||
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
|
||||
var tpl = localStorage.getItem("dlstack-template");
|
||||
if (tpl === "swiss" || tpl === "editorial") document.documentElement.dataset.template = tpl;
|
||||
if (tpl === "swiss" || tpl === "editorial" || tpl === "cosmos") document.documentElement.dataset.template = tpl;
|
||||
} catch (e) {}
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="assets/css/styles.css">
|
||||
<link rel="stylesheet" href="assets/css/swiss.css">
|
||||
<link rel="stylesheet" href="assets/css/cosmos.css">
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Joel Brock — links">
|
||||
|
||||
Reference in New Issue
Block a user