Initial commit: dlstack file-driven link index
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.
This commit is contained in:
499
assets/css/styles.css
Normal file
499
assets/css/styles.css
Normal file
@@ -0,0 +1,499 @@
|
||||
/* dlstack — bolder
|
||||
Editorial poster aesthetic. Massive Fraunces, vermilion that owns the page.
|
||||
*/
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
* { margin: 0; }
|
||||
html, body { height: 100%; }
|
||||
img, svg { display: block; max-width: 100%; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
|
||||
|
||||
:root {
|
||||
--paper: oklch(0.972 0.012 78);
|
||||
--paper-2: oklch(0.945 0.014 78);
|
||||
--ink: oklch(0.190 0.020 268);
|
||||
--ink-2: oklch(0.305 0.020 268);
|
||||
--muted: oklch(0.520 0.014 268);
|
||||
--rule: oklch(0.860 0.010 78);
|
||||
--accent: #E8482C;
|
||||
--on-accent: oklch(0.980 0.014 78);
|
||||
|
||||
--display: "Fraunces", ui-serif, Georgia, serif;
|
||||
--body: "Geist", ui-sans-serif, system-ui, sans-serif;
|
||||
--mono: "Geist Mono", ui-monospace, Menlo, monospace;
|
||||
|
||||
--fs-mini: clamp(0.72rem, 0.70rem + 0.10vw, 0.82rem);
|
||||
--fs-sm: clamp(0.88rem, 0.85rem + 0.15vw, 0.95rem);
|
||||
--fs-md: clamp(1rem, 0.96rem + 0.20vw, 1.10rem);
|
||||
--fs-lg: clamp(1.30rem, 1.18rem + 0.50vw, 1.65rem);
|
||||
--fs-xl: clamp(1.60rem, 1.30rem + 1.20vw, 2.40rem);
|
||||
--fs-tag: clamp(1.50rem, 1.10rem + 1.80vw, 2.80rem);
|
||||
--fs-hero: clamp(4rem, 1.50rem + 13vw, 13rem);
|
||||
--fs-num: clamp(2.50rem, 1.80rem + 3.00vw, 4.50rem);
|
||||
|
||||
--gutter: clamp(1.25rem, 0.75rem + 2.5vw, 3rem);
|
||||
--max: 78rem;
|
||||
--radius: 14px;
|
||||
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-strong: cubic-bezier(0.16, 1, 0.30, 1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-theme="auto"] {
|
||||
--paper: oklch(0.155 0.018 268);
|
||||
--paper-2: oklch(0.205 0.020 268);
|
||||
--ink: oklch(0.97 0.012 78);
|
||||
--ink-2: oklch(0.85 0.014 78);
|
||||
--muted: oklch(0.62 0.018 268);
|
||||
--rule: oklch(0.30 0.020 268);
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--paper: oklch(0.155 0.018 268);
|
||||
--paper-2: oklch(0.205 0.020 268);
|
||||
--ink: oklch(0.97 0.012 78);
|
||||
--ink-2: oklch(0.85 0.014 78);
|
||||
--muted: oklch(0.62 0.018 268);
|
||||
--rule: oklch(0.30 0.020 268);
|
||||
}
|
||||
|
||||
html { font-family: var(--body); color: var(--ink); background: var(--paper); }
|
||||
body {
|
||||
font-size: var(--fs-md);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* paper grain — very subtle */
|
||||
body::before {
|
||||
content: ""; position: fixed; inset: 0;
|
||||
pointer-events: none; z-index: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||||
opacity: 0.6; mix-blend-mode: multiply;
|
||||
}
|
||||
::selection { background: var(--accent); color: var(--on-accent); }
|
||||
|
||||
.shell {
|
||||
position: relative; z-index: 1;
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--gutter) 6rem;
|
||||
}
|
||||
|
||||
/* ───── marker bar ───── */
|
||||
.marker {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
padding: 1.25rem 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
font: 500 var(--fs-mini)/1 var(--mono);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--muted);
|
||||
}
|
||||
.marker__brand { display: inline-flex; align-items: center; gap: 0.6rem; color: var(--accent); margin-right: auto; }
|
||||
.marker__brand .star { font-size: 1.1em; line-height: 0; transform: translateY(1px); }
|
||||
.marker__year { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* theme pill — sits in the marker bar */
|
||||
.theme {
|
||||
padding: 5px 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 999px;
|
||||
font: 500 var(--fs-mini)/1.4 var(--mono);
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
cursor: pointer;
|
||||
transition: border-color 220ms var(--ease), color 220ms var(--ease), background 220ms var(--ease);
|
||||
}
|
||||
.theme:hover { border-color: var(--ink); color: var(--ink); background: color-mix(in oklch, var(--ink) 4%, transparent); }
|
||||
|
||||
/* ───── hero ───── */
|
||||
.hero {
|
||||
position: relative;
|
||||
padding: clamp(3rem, 8vw, 7rem) 0 clamp(3rem, 6vw, 5rem);
|
||||
}
|
||||
.hero__name {
|
||||
font-family: var(--display);
|
||||
font-weight: 320;
|
||||
font-variation-settings: "opsz" 144, "SOFT" 30;
|
||||
font-size: var(--fs-hero);
|
||||
line-height: 0.86;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.hero__name em {
|
||||
font-style: italic;
|
||||
font-variation-settings: "opsz" 144, "SOFT" 100, "WONK" 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
.hero__asterism {
|
||||
position: absolute;
|
||||
top: clamp(2rem, 7vw, 5rem);
|
||||
right: clamp(0.5rem, 3vw, 2rem);
|
||||
font-family: var(--display);
|
||||
font-size: clamp(3rem, 8vw, 6rem);
|
||||
color: var(--accent);
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
transform: rotate(-8deg);
|
||||
opacity: 0.95;
|
||||
}
|
||||
.hero__tagline {
|
||||
margin-top: clamp(1.5rem, 3vw, 2.25rem);
|
||||
font-family: var(--display);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-variation-settings: "opsz" 60, "SOFT" 100;
|
||||
font-size: var(--fs-tag);
|
||||
line-height: 1.1;
|
||||
color: var(--ink-2);
|
||||
letter-spacing: -0.015em;
|
||||
max-width: 24ch;
|
||||
}
|
||||
.hero__tagline span { color: var(--accent); padding: 0 0.2em; }
|
||||
.hero__bio {
|
||||
margin-top: 1.5rem;
|
||||
max-width: 48ch;
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-md);
|
||||
}
|
||||
|
||||
/* ───── social rail ───── */
|
||||
.social { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 2rem; }
|
||||
.social a {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 9px 16px;
|
||||
border: 1px solid var(--ink);
|
||||
border-radius: 999px;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
transition: background 220ms var(--ease-strong), color 220ms var(--ease-strong), transform 220ms var(--ease);
|
||||
}
|
||||
.social a:hover { background: var(--ink); color: var(--paper); transform: translateY(-2px); }
|
||||
.social svg { width: 16px; height: 16px; }
|
||||
|
||||
/* ───── sections ───── */
|
||||
.section { margin-top: clamp(4rem, 7vw, 6rem); }
|
||||
.section__head {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: clamp(1rem, 2.5vw, 2rem);
|
||||
align-items: end;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
|
||||
border-bottom: 2px solid var(--ink);
|
||||
}
|
||||
.section__numwrap { display: flex; flex-direction: column; gap: 0.2rem; align-self: end; }
|
||||
.section__numwrap small {
|
||||
font-family: var(--mono);
|
||||
font-size: var(--fs-mini);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.section__num {
|
||||
font-family: var(--display);
|
||||
font-weight: 350;
|
||||
font-variation-settings: "opsz" 144;
|
||||
font-size: var(--fs-num);
|
||||
line-height: 0.85;
|
||||
color: var(--ink);
|
||||
font-feature-settings: "lnum" on, "tnum" on;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.section__titlewrap {
|
||||
display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
|
||||
gap: 0.75rem 1.5rem;
|
||||
}
|
||||
.section__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 350;
|
||||
font-variation-settings: "opsz" 144, "SOFT" 30;
|
||||
font-size: var(--fs-xl);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.section__kicker {
|
||||
font-family: var(--display);
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
font-size: var(--fs-md);
|
||||
font-variation-settings: "opsz" 24;
|
||||
}
|
||||
|
||||
/* ───── bento grid ───── */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.grid > * { grid-column: span 12; }
|
||||
@media (min-width: 600px) {
|
||||
.grid > .card--link { grid-column: span 6; }
|
||||
.grid > .card--project { grid-column: span 6; }
|
||||
.grid > .card--youtube { grid-column: span 6; }
|
||||
}
|
||||
@media (min-width: 960px) {
|
||||
.grid > .card--link { grid-column: span 4; }
|
||||
.grid > .card--project { grid-column: span 6; }
|
||||
.grid > .card--youtube { grid-column: span 6; }
|
||||
.grid > .card--featured{ grid-column: span 8; }
|
||||
}
|
||||
|
||||
/* portfolio item — always full row */
|
||||
.grid > .card--portfolio { grid-column: span 12; }
|
||||
|
||||
/* clients grid — auto-flowing square tiles */
|
||||
.grid--clients {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
|
||||
gap: clamp(0.75rem, 1.4vw, 1.25rem);
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
.grid--clients { grid-template-columns: repeat(auto-fill, minmax(124px, 1fr)); }
|
||||
}
|
||||
|
||||
/* ───── cards (base) ───── */
|
||||
.card {
|
||||
position: relative;
|
||||
padding: 1.1rem 1.15rem;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: var(--radius);
|
||||
background: var(--paper);
|
||||
transition: transform 320ms var(--ease-strong), border-color 220ms var(--ease), box-shadow 320ms var(--ease);
|
||||
display: flex; flex-direction: column; gap: 0.5rem;
|
||||
isolation: isolate;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--ink-2);
|
||||
box-shadow: 0 1px 0 rgba(20,18,34,0.04), 0 18px 36px -22px rgba(20,18,34,0.30);
|
||||
}
|
||||
.card::after {
|
||||
content: "→";
|
||||
position: absolute; top: 14px; right: 16px;
|
||||
font-family: var(--mono); font-size: 14px;
|
||||
color: var(--muted);
|
||||
transition: transform 320ms var(--ease-strong), color 220ms var(--ease);
|
||||
}
|
||||
.card:hover::after { transform: translate(3px, -3px); color: var(--accent); }
|
||||
|
||||
/* link card */
|
||||
.card--link { display: grid; grid-template-columns: 64px 1fr; gap: 1rem; align-items: start; padding: 1.15rem; padding-right: 2.5rem; }
|
||||
.card--link .favicon {
|
||||
width: 64px; height: 64px; border-radius: 12px;
|
||||
background: var(--paper-2);
|
||||
display: grid; place-items: center;
|
||||
border: 1px solid var(--rule);
|
||||
overflow: hidden;
|
||||
}
|
||||
.card--link .favicon img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.card--link .favicon img.is-favicon { width: 44px; height: 44px; object-fit: contain; }
|
||||
.card--link .favicon[data-fallback] { font-family: var(--display); font-weight: 500; font-size: 1.7rem; color: var(--ink); }
|
||||
.card__title { font-weight: 540; letter-spacing: -0.005em; line-height: 1.25; }
|
||||
.card__desc { color: var(--muted); font-size: var(--fs-sm); }
|
||||
.card__host { font-family: var(--mono); font-size: var(--fs-mini); color: var(--muted); margin-top: 0.2rem; text-transform: lowercase; }
|
||||
|
||||
/* project card */
|
||||
.card--project { padding: 1.5rem; gap: 0.75rem; }
|
||||
.card--project .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 380;
|
||||
font-variation-settings: "opsz" 60;
|
||||
font-size: var(--fs-lg);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
/* FEATURED — accent-filled hero card */
|
||||
.card--featured {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
padding: 2rem 2rem 1.75rem;
|
||||
min-height: clamp(220px, 28vw, 320px);
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card--featured::before {
|
||||
content: "★ Featured";
|
||||
position: absolute; top: 1.25rem; left: 2rem;
|
||||
font-family: var(--mono); font-size: var(--fs-mini);
|
||||
text-transform: uppercase; letter-spacing: 0.2em;
|
||||
color: color-mix(in oklch, var(--on-accent) 75%, transparent);
|
||||
}
|
||||
.card--featured::after { color: color-mix(in oklch, var(--on-accent) 60%, transparent); }
|
||||
.card--featured:hover { transform: translateY(-3px); border-color: var(--accent); box-shadow: 0 1px 0 rgba(20,18,34,0.04), 0 24px 44px -22px color-mix(in oklch, var(--accent) 70%, transparent); }
|
||||
.card--featured:hover::after { color: var(--on-accent); }
|
||||
.card--featured .card__title {
|
||||
font-size: clamp(1.85rem, 1.30rem + 1.6vw, 2.6rem);
|
||||
font-weight: 380;
|
||||
font-variation-settings: "opsz" 144, "SOFT" 30;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--on-accent);
|
||||
margin-top: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.card--featured .card__desc { color: color-mix(in oklch, var(--on-accent) 80%, transparent); font-size: var(--fs-md); max-width: 38ch; }
|
||||
.card--featured .tag {
|
||||
background: color-mix(in oklch, var(--on-accent) 12%, transparent);
|
||||
border-color: color-mix(in oklch, var(--on-accent) 22%, transparent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 0.5rem; }
|
||||
.tag {
|
||||
font-family: var(--mono); font-size: var(--fs-mini);
|
||||
padding: 3px 9px; border-radius: 5px;
|
||||
background: var(--paper-2);
|
||||
color: var(--ink-2);
|
||||
border: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
/* portfolio — wide landscape with image + caption below */
|
||||
.card--portfolio { padding: 0; overflow: hidden; gap: 0; }
|
||||
.card--portfolio::after {
|
||||
top: auto; bottom: 14px; right: 16px;
|
||||
color: var(--paper);
|
||||
text-shadow: 0 1px 8px rgba(0,0,0,0.4);
|
||||
z-index: 2;
|
||||
}
|
||||
.portfolio__media {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: var(--portfolio-ratio, 5 / 2);
|
||||
background: var(--paper-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.portfolio__media img {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 700ms var(--ease-strong);
|
||||
}
|
||||
.card--portfolio:hover .portfolio__media img { transform: scale(1.025); }
|
||||
.portfolio__caption {
|
||||
padding: 1rem 1.25rem 1.1rem;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.portfolio__caption .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 380;
|
||||
font-variation-settings: "opsz" 60;
|
||||
font-size: var(--fs-lg);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
text-transform: none;
|
||||
}
|
||||
.portfolio__caption .card__desc { font-size: var(--fs-sm); }
|
||||
|
||||
/* client tile — square logo, caption below, no card chrome */
|
||||
.card--client {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.card--client::after { display: none; }
|
||||
.card--client:hover { transform: none; box-shadow: none; }
|
||||
.client__logo {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
background: var(--paper-2);
|
||||
border: 1px solid var(--rule);
|
||||
overflow: hidden;
|
||||
display: grid; place-items: center;
|
||||
transition: transform 320ms var(--ease-strong), border-color 220ms var(--ease), box-shadow 320ms var(--ease);
|
||||
}
|
||||
.card--client:hover .client__logo {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--ink-2);
|
||||
box-shadow: 0 1px 0 rgba(20,18,34,0.04), 0 14px 28px -18px rgba(20,18,34,0.28);
|
||||
}
|
||||
.client__logo img.is-favicon { width: 60%; height: 60%; object-fit: contain; }
|
||||
.client__logo img:not(.is-favicon) { width: 100%; height: 100%; object-fit: cover; }
|
||||
.client__logo[data-fallback] { font-family: var(--display); font-weight: 500; font-size: 2.2rem; color: var(--ink); }
|
||||
.client__title {
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--ink-2);
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.005em;
|
||||
/* width is naturally constrained by the grid track (= image width) */
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
/* youtube */
|
||||
.card--youtube { padding: 0; overflow: hidden; }
|
||||
.yt { position: relative; aspect-ratio: 16 / 9; background: #000 center / cover no-repeat; cursor: pointer; display: block; }
|
||||
.yt::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0,0,0,0.65) 100%); }
|
||||
.yt__play {
|
||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
width: 72px; height: 72px; border-radius: 50%;
|
||||
background: rgba(0,0,0,0.55); border: 1.5px solid rgba(255,255,255,0.85);
|
||||
display: grid; place-items: center;
|
||||
transition: transform 320ms var(--ease-strong), background 220ms var(--ease), border-color 220ms var(--ease);
|
||||
}
|
||||
.yt:hover .yt__play { transform: translate(-50%, -50%) scale(1.08); background: var(--accent); border-color: var(--accent); }
|
||||
.yt__play svg { width: 26px; height: 26px; fill: #fff; margin-left: 3px; }
|
||||
.yt__title {
|
||||
position: absolute; left: 16px; right: 16px; bottom: 14px;
|
||||
color: #fff; font-family: var(--display); font-weight: 400;
|
||||
font-size: var(--fs-md); line-height: 1.2;
|
||||
z-index: 1; text-shadow: 0 1px 12px rgba(0,0,0,0.6);
|
||||
}
|
||||
.yt iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
|
||||
|
||||
/* footer */
|
||||
.foot {
|
||||
margin-top: clamp(4rem, 8vw, 7rem);
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--muted);
|
||||
font-family: var(--mono); font-size: var(--fs-mini);
|
||||
text-transform: uppercase; letter-spacing: 0.18em;
|
||||
}
|
||||
.foot__mark {
|
||||
font-family: var(--display); font-style: italic; font-size: 1.4rem;
|
||||
color: var(--accent); text-transform: none; letter-spacing: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.foot__right { text-align: right; }
|
||||
|
||||
/* reveal */
|
||||
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 700ms var(--ease-strong), transform 700ms var(--ease-strong); }
|
||||
.reveal.in { opacity: 1; transform: none; }
|
||||
|
||||
/* hero entrance */
|
||||
.hero__name { animation: rise 1100ms var(--ease-strong) both; }
|
||||
.hero__asterism { animation: spin-in 1400ms 200ms var(--ease-strong) both; }
|
||||
.hero__tagline { animation: rise 1100ms 150ms var(--ease-strong) both; }
|
||||
.hero__bio { animation: rise 1100ms 250ms var(--ease-strong) both; }
|
||||
.social { animation: rise 1100ms 350ms var(--ease-strong) both; }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes spin-in { from { opacity: 0; transform: rotate(-60deg) scale(0.5); } to { opacity: 0.95; transform: rotate(-8deg) scale(1); } }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
|
||||
.reveal { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.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; }
|
||||
407
assets/css/swiss.css
Normal file
407
assets/css/swiss.css
Normal file
@@ -0,0 +1,407 @@
|
||||
/* dlstack — Swiss/International template
|
||||
Activate with theme.template: "swiss" in links.json.
|
||||
Tribute to Müller-Brockmann, Hofmann, Lohse — Akzidenz-Grotesk family
|
||||
(Archivo as a free variable substitute). All overrides scoped to
|
||||
:root[data-template="swiss"] so this file is inert otherwise.
|
||||
*/
|
||||
|
||||
:root[data-template="swiss"] {
|
||||
--paper: oklch(0.985 0 0);
|
||||
--paper-2: oklch(0.955 0 0);
|
||||
--ink: oklch(0.10 0 0);
|
||||
--ink-2: oklch(0.18 0 0);
|
||||
--muted: oklch(0.42 0 0);
|
||||
--rule: oklch(0.10 0 0);
|
||||
--accent: #DC2127;
|
||||
--on-accent: oklch(0.99 0 0);
|
||||
--display: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--body: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--mono: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--radius: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-template="swiss"][data-theme="auto"] {
|
||||
--paper: oklch(0.10 0 0);
|
||||
--paper-2: oklch(0.16 0 0);
|
||||
--ink: oklch(0.985 0 0);
|
||||
--ink-2: oklch(0.92 0 0);
|
||||
--muted: oklch(0.62 0 0);
|
||||
--rule: oklch(0.985 0 0);
|
||||
}
|
||||
}
|
||||
:root[data-template="swiss"][data-theme="dark"] {
|
||||
--paper: oklch(0.10 0 0);
|
||||
--paper-2: oklch(0.16 0 0);
|
||||
--ink: oklch(0.985 0 0);
|
||||
--ink-2: oklch(0.92 0 0);
|
||||
--muted: oklch(0.62 0 0);
|
||||
--rule: oklch(0.985 0 0);
|
||||
}
|
||||
|
||||
/* clean off the editorial paper grain */
|
||||
:root[data-template="swiss"] body::before { display: none; }
|
||||
|
||||
/* ───── marker bar ───── */
|
||||
:root[data-template="swiss"] .marker {
|
||||
border-bottom: 2px solid var(--ink);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .marker__brand { color: var(--ink); gap: 0.55rem; }
|
||||
:root[data-template="swiss"] .marker__brand .star {
|
||||
width: 12px; height: 12px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
font-size: 0; transform: none;
|
||||
}
|
||||
:root[data-template="swiss"] .marker__year { color: var(--accent); }
|
||||
:root[data-template="swiss"] .theme {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
padding: 6px 14px;
|
||||
}
|
||||
:root[data-template="swiss"] .theme:hover { background: var(--ink); color: var(--paper); }
|
||||
|
||||
/* ───── hero — true poster ───── */
|
||||
:root[data-template="swiss"] .hero__name {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.045em;
|
||||
line-height: 0.84;
|
||||
}
|
||||
:root[data-template="swiss"] .hero__name em {
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
color: var(--accent);
|
||||
}
|
||||
/* Replace typographic asterisk with primary geometric mark — solid red disc */
|
||||
:root[data-template="swiss"] .hero__asterism {
|
||||
font-size: 0; color: transparent;
|
||||
width: clamp(3rem, 7vw, 5.5rem);
|
||||
height: clamp(3rem, 7vw, 5.5rem);
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
transform: none;
|
||||
animation: swiss-pop 900ms 200ms cubic-bezier(0.16, 1, 0.30, 1) both;
|
||||
}
|
||||
@keyframes swiss-pop {
|
||||
from { opacity: 0; transform: scale(0.4); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
:root[data-template="swiss"] .hero__tagline {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--ink);
|
||||
font-size: clamp(1.15rem, 0.85rem + 1.4vw, 1.85rem);
|
||||
line-height: 1.1;
|
||||
max-width: 32ch;
|
||||
}
|
||||
:root[data-template="swiss"] .hero__tagline span { color: var(--accent); padding: 0 0.35em; }
|
||||
:root[data-template="swiss"] .hero__bio {
|
||||
color: var(--ink);
|
||||
font-weight: 400;
|
||||
font-family: var(--body);
|
||||
}
|
||||
|
||||
/* ───── social ───── */
|
||||
:root[data-template="swiss"] .social a {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
border-width: 1.5px;
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: var(--fs-mini);
|
||||
padding: 9px 14px;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .social a:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--on-accent);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ───── sections ───── */
|
||||
:root[data-template="swiss"] .section__head {
|
||||
border-bottom: 4px solid var(--ink);
|
||||
padding-bottom: 0.85rem;
|
||||
}
|
||||
:root[data-template="swiss"] .section__numwrap small {
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
font-family: var(--display);
|
||||
}
|
||||
:root[data-template="swiss"] .section__num {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
color: var(--accent);
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
:root[data-template="swiss"] .section__title {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .section__kicker {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ───── cards ───── */
|
||||
:root[data-template="swiss"] .card {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
background: var(--paper);
|
||||
transition: background 200ms cubic-bezier(0.22, 1, 0.36, 1), color 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
isolation: isolate;
|
||||
}
|
||||
:root[data-template="swiss"] .card:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card:hover .card__title,
|
||||
:root[data-template="swiss"] .card:hover .card__desc,
|
||||
:root[data-template="swiss"] .card:hover .card__host { color: var(--paper); }
|
||||
:root[data-template="swiss"] .card::after {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card:hover::after { color: var(--accent); transform: translate(3px, -3px); }
|
||||
|
||||
:root[data-template="swiss"] .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
:root[data-template="swiss"] .card__desc {
|
||||
font-family: var(--body);
|
||||
color: var(--muted);
|
||||
}
|
||||
:root[data-template="swiss"] .card__host {
|
||||
font-family: var(--display);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* link card favicon */
|
||||
:root[data-template="swiss"] .card--link .favicon {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
background: var(--paper);
|
||||
}
|
||||
:root[data-template="swiss"] .card--link:hover .favicon {
|
||||
background: var(--paper);
|
||||
border-color: var(--paper);
|
||||
}
|
||||
:root[data-template="swiss"] .card--link .favicon[data-fallback] {
|
||||
font-family: var(--display);
|
||||
font-weight: 900;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* project card */
|
||||
:root[data-template="swiss"] .card--project .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--fs-lg);
|
||||
line-height: 0.95;
|
||||
}
|
||||
|
||||
/* featured — solid red, white text, true poster */
|
||||
:root[data-template="swiss"] .card--featured {
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
border-color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured:hover {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured::before {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured .card__title {
|
||||
color: var(--on-accent);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(1.95rem, 1.30rem + 1.8vw, 2.8rem);
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured:hover .card__title,
|
||||
:root[data-template="swiss"] .card--featured:hover .card__desc { color: var(--paper); }
|
||||
:root[data-template="swiss"] .card--featured .card__desc {
|
||||
color: color-mix(in oklch, var(--on-accent) 85%, transparent);
|
||||
}
|
||||
|
||||
/* tags — square, monoline, all caps */
|
||||
:root[data-template="swiss"] .tag {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
background: var(--paper);
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card:hover .tag {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
border-color: var(--paper);
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured .tag {
|
||||
background: color-mix(in oklch, var(--on-accent) 14%, transparent);
|
||||
border-color: color-mix(in oklch, var(--on-accent) 30%, transparent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
:root[data-template="swiss"] .card--featured:hover .tag {
|
||||
background: color-mix(in oklch, var(--paper) 14%, transparent);
|
||||
border-color: color-mix(in oklch, var(--paper) 30%, transparent);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
/* youtube */
|
||||
:root[data-template="swiss"] .card--youtube {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .yt__play {
|
||||
border-radius: 0;
|
||||
border-width: 2px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
}
|
||||
:root[data-template="swiss"] .yt:hover .yt__play {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
:root[data-template="swiss"] .yt__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* portfolio */
|
||||
:root[data-template="swiss"] .card--portfolio {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card--portfolio:hover { background: var(--paper); color: var(--ink); }
|
||||
:root[data-template="swiss"] .card--portfolio:hover .portfolio__caption .card__title { color: var(--accent); }
|
||||
:root[data-template="swiss"] .portfolio__caption {
|
||||
border-top: 2px solid var(--ink);
|
||||
padding: 1.1rem 1.35rem 1.2rem;
|
||||
}
|
||||
:root[data-template="swiss"] .portfolio__caption .card__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 900;
|
||||
font-variation-settings: normal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
font-size: var(--fs-lg);
|
||||
line-height: 1;
|
||||
}
|
||||
:root[data-template="swiss"] .portfolio__caption .card__desc { color: var(--muted); }
|
||||
|
||||
/* clients */
|
||||
:root[data-template="swiss"] .client__logo {
|
||||
border-radius: 0;
|
||||
border-color: var(--ink);
|
||||
background: var(--paper);
|
||||
}
|
||||
/* opt the client tile out of the global card hover-flip: the card has no
|
||||
chrome, so filling it with ink would hide the title text below the logo */
|
||||
:root[data-template="swiss"] .card--client:hover {
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
border-color: transparent;
|
||||
}
|
||||
:root[data-template="swiss"] .card--client:hover .client__title { color: var(--accent); }
|
||||
:root[data-template="swiss"] .card--client:hover .client__logo {
|
||||
background: var(--ink);
|
||||
border-color: var(--ink);
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:root[data-template="swiss"] .card--client:hover .client__logo img.is-favicon { filter: invert(1); }
|
||||
:root[data-template="swiss"] .client__logo[data-fallback] {
|
||||
font-family: var(--display);
|
||||
font-weight: 900;
|
||||
color: var(--ink);
|
||||
}
|
||||
:root[data-template="swiss"] .card--client:hover .client__logo[data-fallback] { color: var(--paper); }
|
||||
:root[data-template="swiss"] .client__title {
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: var(--fs-mini);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* footer */
|
||||
:root[data-template="swiss"] .foot {
|
||||
border-top: 2px solid var(--ink);
|
||||
color: var(--ink);
|
||||
font-family: var(--display);
|
||||
font-weight: 600;
|
||||
}
|
||||
:root[data-template="swiss"] .foot__mark {
|
||||
font-family: var(--display);
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
font-weight: 900;
|
||||
color: var(--accent);
|
||||
font-size: var(--fs-md);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* selection */
|
||||
:root[data-template="swiss"] ::selection {
|
||||
background: var(--accent);
|
||||
color: var(--on-accent);
|
||||
}
|
||||
|
||||
/* override editorial entrance for the asterism (it spins; Swiss should pop) */
|
||||
:root[data-template="swiss"] .hero__asterism { animation: swiss-pop 900ms 200ms cubic-bezier(0.16, 1, 0.30, 1) both; }
|
||||
303
assets/js/app.js
Normal file
303
assets/js/app.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* dlstack — distilled
|
||||
* Loads data/links.json and renders sections, cards, YouTube facades.
|
||||
* No search, no filter, no clock, no theme toggle. The page is the index.
|
||||
*/
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const $ = (s, el = document) => el.querySelector(s);
|
||||
const $$ = (s, el = document) => Array.from(el.querySelectorAll(s));
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||||
}[c]));
|
||||
const frag = (html) => document.createRange().createContextualFragment(html);
|
||||
|
||||
const hostOf = (url) => {
|
||||
try { return new URL(url, location.href).hostname.replace(/^www\./, ""); }
|
||||
catch { return ""; }
|
||||
};
|
||||
const faviconFor = (url) => {
|
||||
const h = hostOf(url);
|
||||
return h ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(h)}&sz=128` : "";
|
||||
};
|
||||
const ytThumb = (id) => `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg`;
|
||||
|
||||
function renderLink(it) {
|
||||
const host = hostOf(it.url);
|
||||
const custom = it.image || it.icon;
|
||||
const src = custom || faviconFor(it.url);
|
||||
const isFavicon = !custom;
|
||||
const initial = (it.title || host || "·").trim().charAt(0).toUpperCase();
|
||||
return `
|
||||
<a class="card card--link reveal" href="${esc(it.url)}" target="_blank" rel="noopener noreferrer">
|
||||
<span class="favicon" ${src ? "" : "data-fallback"}>
|
||||
${src
|
||||
? `<img loading="lazy" alt="" src="${esc(src)}" data-fallback-initial="${esc(initial)}"${isFavicon ? ' class="is-favicon"' : ""}>`
|
||||
: esc(initial)}
|
||||
</span>
|
||||
<span>
|
||||
<span class="card__title">${esc(it.title)}</span>
|
||||
${it.description ? `<span class="card__desc">${esc(it.description)}</span>` : ""}
|
||||
${host ? `<span class="card__host">${esc(host)}</span>` : ""}
|
||||
</span>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
function renderProject(it) {
|
||||
const featured = it.featured ? " card--featured" : "";
|
||||
const tag = it.url ? "a" : "div";
|
||||
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
|
||||
const tags = (it.tags || []).map(t => `<span class="tag">${esc(t)}</span>`).join("");
|
||||
return `
|
||||
<${tag} class="card card--project${featured} reveal" ${attrs}>
|
||||
<span class="card__title">${esc(it.title)}</span>
|
||||
${it.description ? `<p class="card__desc">${esc(it.description)}</p>` : ""}
|
||||
${tags ? `<div class="tags">${tags}</div>` : ""}
|
||||
</${tag}>`;
|
||||
}
|
||||
|
||||
function renderYouTube(it) {
|
||||
return `
|
||||
<div class="card card--youtube reveal">
|
||||
<div class="yt" role="button" tabindex="0" aria-label="Play: ${esc(it.title)}" data-yt="${esc(it.id)}" style="background-image:url('${esc(ytThumb(it.id))}')">
|
||||
<div class="yt__play" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
|
||||
</div>
|
||||
<div class="yt__title">${esc(it.title)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPortfolio(it) {
|
||||
const src = it.image || it.url;
|
||||
const tag = it.url ? "a" : "div";
|
||||
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
|
||||
const ratio = it.ratio ? ` style="--portfolio-ratio:${esc(String(it.ratio).replace(":", " / "))}"` : "";
|
||||
return `
|
||||
<${tag} class="card card--portfolio reveal" ${attrs}>
|
||||
<div class="portfolio__media"${ratio}>
|
||||
${src ? `<img loading="lazy" alt="${esc(it.title || "")}" src="${esc(src)}">` : ""}
|
||||
</div>
|
||||
${(it.title || it.description) ? `
|
||||
<div class="portfolio__caption">
|
||||
${it.title ? `<span class="card__title">${esc(it.title)}</span>` : ""}
|
||||
${it.description ? `<span class="card__desc">${esc(it.description)}</span>` : ""}
|
||||
</div>` : ""}
|
||||
</${tag}>`;
|
||||
}
|
||||
|
||||
function renderClient(it) {
|
||||
const host = hostOf(it.url);
|
||||
const custom = it.image || it.icon;
|
||||
const src = custom || faviconFor(it.url);
|
||||
const isFavicon = !custom;
|
||||
const initial = (it.title || host || "·").trim().charAt(0).toUpperCase();
|
||||
const tag = it.url ? "a" : "div";
|
||||
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
|
||||
return `
|
||||
<${tag} class="card card--client reveal" ${attrs} title="${esc(it.title || host || "")}">
|
||||
<span class="client__logo" ${src ? "" : "data-fallback"}>
|
||||
${src
|
||||
? `<img loading="lazy" alt="${esc(it.title || host || "")}" src="${esc(src)}" data-fallback-initial="${esc(initial)}"${isFavicon ? ' class="is-favicon"' : ""}>`
|
||||
: esc(initial)}
|
||||
</span>
|
||||
${it.title ? `<span class="client__title">${esc(it.title)}</span>` : ""}
|
||||
</${tag}>`;
|
||||
}
|
||||
|
||||
const renderItem = (it) =>
|
||||
it.type === "youtube" ? renderYouTube(it) :
|
||||
it.type === "card" ? renderProject(it) :
|
||||
it.type === "client" ? renderClient(it) :
|
||||
it.type === "portfolio" ? renderPortfolio(it) :
|
||||
renderLink(it);
|
||||
|
||||
function renderSection(sec, n) {
|
||||
const items = (sec.items || []).map(renderItem).join("");
|
||||
const num = String(n).padStart(2, "0");
|
||||
const isClients = sec.layout === "clients" || sec.items?.[0]?.type === "client";
|
||||
const gridClass = isClients ? "grid--clients" : "grid";
|
||||
return `
|
||||
<section class="section">
|
||||
<header class="section__head">
|
||||
<div class="section__numwrap">
|
||||
<small>№</small>
|
||||
<span class="section__num">${num}</span>
|
||||
</div>
|
||||
<div class="section__titlewrap">
|
||||
<h2 class="section__title">${esc(sec.label)}</h2>
|
||||
${sec.kicker ? `<span class="section__kicker">${esc(sec.kicker)}</span>` : ""}
|
||||
</div>
|
||||
</header>
|
||||
<div class="${gridClass}">${items}</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
const SOCIAL_ICONS = {
|
||||
github: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5a11.5 11.5 0 0 0-3.64 22.41c.58.1.79-.25.79-.56v-2c-3.2.7-3.87-1.37-3.87-1.37-.52-1.34-1.28-1.7-1.28-1.7-1.05-.71.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.7 0-1.26.45-2.3 1.19-3.1-.12-.3-.52-1.48.11-3.08 0 0 .97-.31 3.18 1.18a11.05 11.05 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.6.23 2.78.11 3.08.74.8 1.18 1.84 1.18 3.1 0 4.43-2.69 5.41-5.25 5.69.41.36.78 1.06.78 2.14v3.17c0 .31.2.67.8.56A11.5 11.5 0 0 0 12 .5Z"/></svg>',
|
||||
linkedin: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5A2.5 2.5 0 1 1 4.97 8.5 2.5 2.5 0 0 1 4.98 3.5ZM3 9.75h4V21H3V9.75ZM9.5 9.75h3.8v1.55h.05c.53-1 1.83-2.05 3.77-2.05 4.03 0 4.78 2.65 4.78 6.1V21H18V16.1c0-1.17-.02-2.68-1.63-2.68-1.63 0-1.88 1.27-1.88 2.6V21H9.5V9.75Z"/></svg>',
|
||||
mail: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>',
|
||||
rss: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 3a16 16 0 0 1 16 16h-3A13 13 0 0 0 5 6V3Zm0 7a9 9 0 0 1 9 9h-3a6 6 0 0 0-6-6v-3Zm1.5 6a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z"/></svg>',
|
||||
link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg>',
|
||||
calendar: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-days-icon lucide-calendar-days"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>',
|
||||
bluesky: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bluesky</title><path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"/></svg>'
|
||||
};
|
||||
|
||||
function renderSocial(items) {
|
||||
return items.map(s =>
|
||||
`<a href="${esc(s.url)}" target="_blank" rel="noopener noreferrer">${SOCIAL_ICONS[s.icon] || SOCIAL_ICONS.link}<span>${esc(s.label)}</span></a>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
function nameMarkup(name) {
|
||||
if (!name) return "";
|
||||
const parts = String(name).trim().split(/\s+/);
|
||||
if (parts.length < 2) return esc(name);
|
||||
const last = parts.pop();
|
||||
return `${esc(parts.join(" "))} <em>${esc(last)}</em>`;
|
||||
}
|
||||
|
||||
function attachFaviconFallback(root) {
|
||||
$$("img[data-fallback-initial]", root).forEach(img => {
|
||||
img.addEventListener("error", () => {
|
||||
const initial = img.dataset.fallbackInitial || "·";
|
||||
const parent = img.parentElement;
|
||||
if (parent) { parent.setAttribute("data-fallback", ""); parent.textContent = initial; }
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function attachYouTube(root) {
|
||||
const open = (fac) => {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(fac.dataset.yt)}?autoplay=1&rel=0`;
|
||||
iframe.allow = "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture";
|
||||
iframe.allowFullscreen = true;
|
||||
iframe.title = fac.getAttribute("aria-label") || "YouTube video";
|
||||
fac.replaceChildren(iframe);
|
||||
fac.style.cursor = "default";
|
||||
};
|
||||
root.addEventListener("click", (e) => {
|
||||
const fac = e.target.closest("[data-yt]");
|
||||
if (fac) { e.preventDefault(); open(fac); }
|
||||
});
|
||||
root.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
const fac = e.target.closest("[data-yt]");
|
||||
if (fac) { e.preventDefault(); open(fac); }
|
||||
});
|
||||
}
|
||||
|
||||
function attachTheme() {
|
||||
const root = document.documentElement;
|
||||
const btn = $("#theme");
|
||||
if (!btn) return;
|
||||
const order = ["auto", "light", "dark"];
|
||||
const label = { auto: "Auto", light: "Light", dark: "Dark" };
|
||||
const stored = localStorage.getItem("dlstack-theme");
|
||||
if (!order.includes(root.dataset.theme)) root.dataset.theme = "auto";
|
||||
if (order.includes(stored)) root.dataset.theme = stored;
|
||||
const sync = () => {
|
||||
btn.textContent = label[root.dataset.theme];
|
||||
btn.setAttribute("aria-label", `Theme: ${label[root.dataset.theme]}. Click to change.`);
|
||||
};
|
||||
sync();
|
||||
btn.addEventListener("click", () => {
|
||||
const i = order.indexOf(root.dataset.theme);
|
||||
root.dataset.theme = order[(i + 1) % order.length];
|
||||
localStorage.setItem("dlstack-theme", root.dataset.theme);
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
function attachReveal(root) {
|
||||
if (!("IntersectionObserver" in window)) {
|
||||
$$(".reveal", root).forEach(el => el.classList.add("in"));
|
||||
return;
|
||||
}
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach(en => {
|
||||
if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); }
|
||||
});
|
||||
}, { threshold: 0.08, rootMargin: "0px 0px -40px 0px" });
|
||||
$$(".reveal", root).forEach(el => io.observe(el));
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const sources = ["data/links.json", "data/links.example.json"];
|
||||
let lastErr;
|
||||
for (const src of sources) {
|
||||
try {
|
||||
const res = await fetch(src, { cache: "no-cache" });
|
||||
if (!res.ok) { lastErr = new Error(`${src}: HTTP ${res.status}`); continue; }
|
||||
return await res.json();
|
||||
} catch (err) { lastErr = err; }
|
||||
}
|
||||
throw lastErr || new Error("No data file found.");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const app = $("#app");
|
||||
let data;
|
||||
try {
|
||||
data = await loadData();
|
||||
} catch (err) {
|
||||
const p = document.createElement("p");
|
||||
p.style.cssText = "color:var(--accent);font-family:var(--mono);padding:2rem;max-width:48ch";
|
||||
p.textContent = `Couldn't load data/links.json or data/links.example.json. Check that one exists and is valid JSON. Details: ${err.message}`;
|
||||
app.replaceChildren(p);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.theme?.accent) {
|
||||
document.documentElement.style.setProperty("--accent", data.theme.accent);
|
||||
}
|
||||
const tpl = data.theme?.template === "swiss" ? "swiss" : "editorial";
|
||||
document.documentElement.dataset.template = tpl;
|
||||
try { localStorage.setItem("dlstack-template", tpl); } catch (e) {}
|
||||
|
||||
const p = data.profile || {};
|
||||
const sections = data.sections || [];
|
||||
const social = data.social || [];
|
||||
document.title = `${p.name || "Links"}`;
|
||||
|
||||
const taglineMarkup = (t) =>
|
||||
esc(t).replace(/\s*[·•|]\s*/g, '<span aria-hidden="true">·</span>');
|
||||
|
||||
const html = `
|
||||
<div class="marker">
|
||||
<span class="marker__brand"><span class="star" aria-hidden="true">✱</span> Index №01</span>
|
||||
<span class="marker__year">MMXXVI</span>
|
||||
<button id="theme" class="theme" type="button">Auto</button>
|
||||
</div>
|
||||
|
||||
<header class="hero">
|
||||
<span class="hero__asterism" aria-hidden="true">✱</span>
|
||||
<h1 class="hero__name">${nameMarkup(p.name)}</h1>
|
||||
${p.tagline ? `<p class="hero__tagline">${taglineMarkup(p.tagline)}</p>` : ""}
|
||||
${p.bio ? `<p class="hero__bio">${esc(p.bio)}</p>` : ""}
|
||||
${social.length ? `<nav class="social" aria-label="Social">${renderSocial(social)}</nav>` : ""}
|
||||
</header>
|
||||
|
||||
<main>${sections.map((s, i) => renderSection(s, i + 1)).join("")}</main>
|
||||
|
||||
<footer class="foot">
|
||||
<span>${esc(data.footer?.copy || "")}</span>
|
||||
<span class="foot__mark" aria-hidden="true">— ✱ —</span>
|
||||
<span class="foot__right">© ${new Date().getFullYear()} ${esc(p.name || "")}</span>
|
||||
</footer>
|
||||
`;
|
||||
app.replaceChildren(frag(html));
|
||||
|
||||
attachFaviconFallback(app);
|
||||
attachYouTube(app);
|
||||
attachReveal(app);
|
||||
attachTheme();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", main);
|
||||
} else {
|
||||
main();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user