Reverted the global youtube wide treatment per user direction.
Videos default to span 6 (pair layout) at 600px+ again. Items with
`featured: true` in JSON get a new `card--wide` class that opts
them into the same 78%-centered + vertical-breathing layout
portfolio uses — meant to promote a hero video without making
every video imposing.
YouTube cards only have absolutely-positioned children
(.yt__play, .yt__title), so their intrinsic width is 0. With
max-width:78% + justify-self:center but no `width`, the cards
collapsed to nothing — explaining the empty Videos section.
Portfolio worked because its caption has text content giving the
card an intrinsic width.
Add width:100% to the constrained rule so max-width has a definite
basis to cap against. Both portfolio and youtube cards now render
at 78% of their grid track on 700px+.
YouTube cards now follow the portfolio pattern: full-bleed on
mobile, 78% max-width centered on 700px+, with margin-block to keep
neighbors at distance. Dropped the old span 6 (2-up pair) rules at
the 600/960px breakpoints — videos read better as deliberate single
plates than as a grid wall now that they take up real space.
Portfolio cards no longer span the full 12-column row on tablet+
sizes — they cap at 78% max-width and center themselves, so a
landscape image feels less like a wall and more like a plate inside
the layout. Stays full-bleed below 700px so it still feels right on
phones. Added margin-block to push neighbors away vertically.
Targeted pass per impeccable:arrange — no markup changes, just sharper
spatial intent in the editorial base:
- New semantic spacing scale (--space-2xs through --space-3xl) at
the top of :root. Subsequent rules pull from this scale instead of
inline clamp() values; the scattered numbers stay only where they
are type-specific (card padding, etc.).
- Hero internal rhythm rebalanced. Tagline keeps its generous gap
(--space-xl), but bio now sits TIGHT against it (--space-sm) so
they read as one thought, while the social rail jumps to
--space-xl from the bio so it reads as a separate action layer.
Previous mid-range 1.5/1.5/2rem values made everything feel
equally connected.
- Opening beat: first .section after the hero (and the first child
of <main>) gets --space-3xl top margin instead of --space-2xl, so
the page actually breathes after the name before the masthead
cadence settles in.
- Section masthead tightened: head gap nudged up, title weight
350 -> 420 with a touch tighter tracking so Projects / Clients
carry their poster weight.
- Marker bar pads asymmetrically now (--space-lg top, --space-md
bottom) — typographic line of muted caps sits closer to the rule
than to the page top, intentional.
- .card--client gets 0.85rem of bottom padding so wrapping client
names ("Perennial Promise Growers' Cooperative", "Common Good
Management Services") no longer butt up against the bottom edge of
the tile row.
- .section--headless top margin grows ~2x (clamp 1.25-2 -> 2.75-4.5)
so a nested testimonial sits comfortably below the section it's
attached to.
- New .section--headless + .section selector shrinks the next
section's top margin so the testimonial's bottom whitespace
doesn't double-pad before whatever comes after it.
- Removed the cosmos turtle (CSS rules + auto-load probe + JS hook).
Reduced-motion turtle branch removed too.
- ?template=editorial|swiss|cosmos URL param now wins over the value
in data/links.json (and the cached localStorage fallback). Wired in
both the pre-paint boot script and main() so deep links work
without flashing the wrong template.
- New hidden easter egg: type the letters w-a-r-p anywhere on the
page (no input focused) and the page reloads on the next template
in the cycle editorial -> swiss -> cosmos -> editorial. The URL is
updated with the new ?template= param so the override survives
refresh and is shareable. A small "↯ <name>" toast pops up after
the reload via sessionStorage handoff, fades after ~2.5s, respects
prefers-reduced-motion.
- README documents both the URL param and the warp keyword.
Pushed the cosmos theme further:
- Cursor aurora — 600px soft cyan/violet/magenta halo follows the
pointer with lag (mix-blend-mode: screen, blur 8px). Fades in on
first move, fades out on pointerleave.
- Card 3D tilt — every cosmos card rotates up to ~6deg toward the
cursor on hover via custom-property-driven rotateX/rotateY, with a
cursor-tracking radial shimmer painted inside via mix-blend-mode:
screen. RAF-throttled. Skipped for testimonial + client tiles since
they have no chrome.
- Hero parallax — name, tagline, and drift-orb all shift relative to
cursor position via --cm-x / --cm-y custom properties set globally
from the pointer move handler.
- Shader: doubled the gravity-well strength (0.03 -> 0.06) and added
a click-driven ripple — pointerdown sets u_ripple_pos/u_ripple_age
uniforms; the shader propagates a cyan/magenta luminous ring of
displacement that decays over ~2s.
- Comets — 3 CSS-only streaks crossing the page on staggered 14/22/
19s loops with cyan/violet drop-shadow trails.
- Turtle — new .cosmos-turtle img auto-loads if assets/img/turtle.png
resolves (or theme.turtle URL is set). Floats in from the left,
arcs across, fades out off the right; pulses its aurora glow on a
4s bob. Reduced-motion users see it positioned statically.
- All new chrome respects prefers-reduced-motion: halo/comets hidden,
card tilt + parallax disabled, turtle pinned static.
Also adds section.headless = true (per user request): renders the
section body with no header/kicker chrome and a tighter top margin so
the contents read as nested under the prior section. Example JSON now
uses this to slide the testimonials carousel under the clients wall.
User wanted the testimonial section to feel "of the page" rather than
a card sitting on top of it, and to occupy less vertical space.
- Strip all chrome from .card--testimonial: transparent background,
no border, no shadow, no hover lift. Same treatment applied in the
swiss + cosmos overrides so per-theme backgrounds don't sneak back.
- Restructure the carousel markup to put the prev/next buttons
directly beside the viewport (carousel--inline = flex row), no
separate controls block, no dot indicators.
- Shrink the curly quote mark and the avatar (44 -> 32px); tighten
internal gaps. Removed the rule above the attribution row.
- Keep healthy horizontal padding via clamp() so the arrows don't
crowd the surrounding sections.
- Update aria-label on the carousel itself instead of the dots so
screen readers still get position context.
Center-align the testimonial card (text + attribution row), then
size the quote based on character count so short, punchy quotes get
the spotlight and long ones tighten down:
is-xshort (<=90 chars) 3rem cap
is-short (<=180 chars) 2.25rem cap
is-medium (<=320 chars) 1.55rem cap (the previous baseline)
is-long (<=520 chars) 1.20rem cap
is-xlong (>520 chars) 1.05rem cap
Counts use [...str] so grapheme-cluster emoji (skin-tone, ZWJ) count
as a single visual character instead of inflating the length.
Crossfade: drop the secondary translateY (it stuttered against
centered content), bump duration 600ms -> 1100ms, swap easing to
cubic-bezier(0.65, 0, 0.35, 1) for a softer in/out, and add
will-change:opacity so the browser keeps the layer on the GPU.
New `type: "testimonial"` with structured attribution
(quote/name/role/org/url/image/date) plus a `layout: "testimonials"`
section behavior that renders the items as a tasteful crossfade
carousel — one quote at a time, 7s auto-advance, prev/next nav, dot
indicators, swipe support, ←/→ keys, pause on hover/focus, ARIA
live region.
Reduced-motion users automatically get .carousel--stacked: every
testimonial visible at once, controls hidden, no auto-advance. A
single-item section also skips the carousel chrome.
Per-template treatment: editorial uses Fraunces italic for the curly
quote mark and the body, swiss strips uppercase from titles per prior
fix, cosmos glows the mark with the cyan/violet accent stack.
Section auto-detects the layout when the first item is a testimonial,
matching how the clients layout already works.
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.
The .yt::after gradient overlay was sitting on top of the inserted
iframe and stealing every click — clicks on the YouTube player controls
bubbled to the .yt facade, which still matched [data-yt] and re-ran
open(), replacing the iframe and restarting playback from 0.
- Add pointer-events:none on .yt::after so it never blocks player clicks
- On play, swap .yt to a .yt--playing state that hides the overlay,
play button, and title overlay
- Remove the data-yt/role/tabindex attrs after init so any leftover
clicks on the facade no longer match the open() delegate
- Add controls=1 explicitly and broaden the allow list (clipboard-write,
web-share) for the embedded player
Swiss: drop all-caps on titles, soften heavy ink borders to muted rules
or remove them where background contrast carries the form, balance hero
name weights (smaller lighter first name vs heavier italic last name),
fix unreadable white description text on portfolio hover.
Editorial: enlarge the section "No" mark and set it in italic Fraunces so
it reads as typography instead of a tick, swap the first name into DM
Serif Display for a different J, sharpen the section number caption.
Client tile: contain custom logos at 86% so wide marks like
"The Cooperative Way" stop getting cropped to "operative".
New: optional `date` field on every item type (link/card/portfolio/
youtube/client). Accepts YYYY, YYYY-MM, or YYYY-MM-DD. Rendered
human-friendly with the raw ISO preserved in <time datetime>.
Documented in README and demonstrated in links.example.json.
A hand-rolled linktree alternative — pure static HTML/CSS/JS, no build
step. Drop on any shared host via SFTP and edit data/links.json to update.
Features
- File-driven content via data/links.json (links, projects, YouTube,
client tiles, portfolio pieces)
- Two interchangeable templates: editorial (Fraunces + paper + vermilion)
and swiss (Archivo grotesque, all-caps poster)
- Auto/light/dark theme toggle with no-flash boot script
- Auto-fetched favicons via Google S2 (with image-URL override)
- Lazy YouTube facades (no third-party JS until clicked)
- Adaptive client-logo grid
- Scroll-triggered reveal animations
- ~40 KB total payload, ~12 KB gzipped
The repo ships links.example.json as a demo; data/links.json is
gitignored so personal content stays out of the public repo.