Layout audit: wasted space removed on every page, compact OpenVibe group, steadier phones
Changes
@@@ -28,7 +28,8 @@ import profile from "../data/profile.json";</section><style>-- .ct { overflow: hidden; padding: clamp(28px, 5vw, 60px); }++ .ct { overflow: hidden; padding: clamp(20px, 4vw, 48px); }++ @media (max-width: 560px) { .ct__actions { display: grid; grid-template-columns: 1fr; } .ct__actions .btn { justify-content: center; } }.ct__glow { position: absolute; inset: -40% -20% auto auto; width: 60%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(closest-side, rgba(var(--accent-rgb), .35), transparent 70%); filter: blur(30px); animation: breathe 9s ease-in-out infinite; pointer-events: none; }@keyframes breathe { 50% { transform: scale(1.15) translate(-6%, 8%); opacity: .7; } }.ct__grid { position: relative; display: grid; grid-template-columns: 1.3fr 1fr; gap: clamp(24px, 4vw, 56px); align-items: center; }@@@ -24,11 +24,6 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);<div class="now__grid"><article class="card spot now__pitch" data-reveal><p class="now__text">{pitchParts.map((p) => typeof p === "string" ? p : <a href={p.href} target={p.external ? "_blank" : undefined} rel={p.external ? "noopener" : undefined}>{p.text}</a>)}</p>-- <ul class="now__hl" aria-label="What I bring">-- {profile.highlights.map((h) => (-- <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>-- ))}-- </ul><p class="now__text">Ten years of running my own company taught me to care about the whole loop: the code, the servers, the invoice and the person on the other end. I like small teams, hard problems and software that stays up.</p><div class="now__cta"><a href="#experience" class="btn btn--ghost">Experience <span class="btn__arrow">→</span></a>@@@ -36,6 +31,12 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);</div></article>++ <ul class="now__hl" aria-label="What I bring" data-stagger>++ {profile.highlights.map((h) => (++ <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>++ ))}++ </ul>++<aside class="now__side"><div class="card spot now__status" data-reveal data-delay="100"><div class="now__status-head"><span class="live-dot"></span><span class="eyebrow">Right now</span></div>@@@ -61,17 +62,22 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);</section><style>-- .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 18px; align-items: start; }++ /* The pitch and the side cards share the first row; the five strengths run under both as their own++ row, so neither column ends far above the other. Phones: pitch, strengths, then the side cards. */++ .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; grid-template-areas: "pitch side" "hl hl"; gap: 18px; align-items: start; }++ .now__pitch { grid-area: pitch; } .now__side { grid-area: side; } .now__hl { grid-area: hl; }.now__pitch { padding: clamp(24px, 3.5vw, 40px); }.now__text { font-size: 1.08rem; line-height: 1.8; color: var(--muted); }.now__text:first-child { color: var(--fg); font-size: clamp(1.1rem, 1.6vw, 1.3rem); font-weight: 500; line-height: 1.6; letter-spacing: -.005em; }-- .now__hl { list-style: none; margin: 22px 0 18px; padding: 0; display: grid; gap: 10px; }++ .now__hl { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }.now__hl li { display: flex; gap: 14px; align-items: flex-start; padding: 12px 14px; border-radius: 14px; background: var(--surface); border: 1px solid var(--border); transition: border-color .3s var(--ease), transform .3s var(--ease); }++ @media (min-width: 901px) { .now__hl li { flex-direction: column; gap: 10px; } }.now__hl li:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateX(4px); }.now__hl i { flex: none; width: 36px; height: 36px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .12); color: var(--accent); font-size: .95rem; }.now__hl strong { display: block; color: var(--fg); font-size: .98rem; margin-bottom: 2px; }.now__hl span { color: var(--muted); font-size: .9rem; line-height: 1.55; }-- .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }++ .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }++ @media (max-width: 560px) { .now__cta { display: grid; grid-template-columns: 1fr 1fr; } .now__cta .btn { justify-content: center; padding: 12px 14px; } .now__pitch { padding: 20px; } }.now__side { display: grid; gap: 18px; }.now__status-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }.now__status-list, .now__seek { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }@@@ -86,5 +92,5 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);.now__seek li:first-child { border-top: 0; padding-top: 0; }.now__seek strong { color: var(--fg); font-size: .95rem; }.now__seek span { color: var(--muted); font-size: .86rem; }-- @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; } }++ @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; grid-template-areas: "pitch" "hl" "side"; } }</style>@@@ -7,6 +7,8 @@ const icons: Record<string, string> = {live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film",tools: "fa-solid fa-toolbox", games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",};++// "OpenVibe.Live" -> "OpenVibe." + "Live": phones show only the short name (the group already says OpenVibe).++const short = (n: string) => n.startsWith("OpenVibe.") ? n.slice(9) : null;// Architecture diagram geometry (viewBox 800 x 420)const hub = { x: 400, y: 210 };const nodes = [@@@ -34,7 +36,7 @@ const edges = [<span class="tile__icon"><i class={icons[big.key]}></i></span><span class="tile__role">{big.role}</span></div>-- <h3 class="tile__name">{big.name}</h3>++ <h3 class="tile__name">{short(big.name) ? <><span class="tile__pre">OpenVibe.</span>{short(big.name)}</> : big.name}</h3><p class="tile__blurb">{big.blurb}</p><div class="tile__foot"><span class="tile__stat mono">{big.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div></div>@@@ -48,7 +50,7 @@ const edges = [<span class="tile__icon"><i class={icons[s.key]}></i></span><span class="tile__role">{s.role}</span></div>-- <h3 class="tile__name">{s.name}</h3>++ <h3 class="tile__name">{short(s.name) ? <><span class="tile__pre">OpenVibe.</span>{short(s.name)}</> : s.name}</h3><p class="tile__blurb">{s.blurb}</p><div class="tile__foot"><span class="tile__stat mono">{s.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div></div>@@@ -60,7 +62,10 @@ const edges = [<div class="arch__copy"><div class="eyebrow"><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> How it fits together</div><h3>One login, one wallet, seven products that feel like one.</h3>++ <details class="arch__more" open>++ <summary>Read more</summary><p>Sign in once and every OpenVibe site knows who you are, what you own and what you look like. The identity service in the middle handles accounts, security and the shared virtual currency; every other product plugs into it through clear, documented APIs. That's the same playbook I bring to any company: secure single sign-on, payments and APIs designed so new products can be added without breaking the old ones.</p>++ </details><ul class="arch__facts"><li><i class="fa-solid fa-rotate"></i> Updates ship with zero downtime, so users never see an outage</li><li><i class="fa-solid fa-cloud-arrow-up"></i> Media storage tiers automatically from fast local disks to cheap cloud storage</li>@@@ -107,7 +112,6 @@ const edges = [min-height: 210px; border-radius: var(--radius-lg); text-decoration: none; color: var(--fg);background: var(--surface); border: 1px solid var(--border);transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease);-- will-change: transform;}.tile:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .45); box-shadow: var(--shadow-card), var(--shadow-glow); }.tile--big { grid-column: span 2; grid-row: span 2; min-height: 440px; }@@@ -131,11 +135,14 @@ const edges = [.tile--sites { background: var(--grad-soft); }.tile--sites::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 60% at 80% 20%, rgba(var(--accent-2-rgb), .25), transparent 70%); }@media (max-width: 1024px) { .bento { grid-template-columns: repeat(2, 1fr); } .tile--big { grid-column: span 2; grid-row: span 1; min-height: 360px; } .tile--community, .tile--sites { grid-column: span 1; } }++ /* The diagram is unreadable at phone width; the facts beside it say the same thing. */++ @media (max-width: 560px) { .arch__svg { display: none; } .arch { padding: 18px; } }@media (max-width: 560px) { .bento { grid-template-columns: 1fr; } .tile--big, .tile--community, .tile--sites { grid-column: span 1; } .tile--big { min-height: 320px; } }/* Architecture */.arch { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(20px, 4vw, 48px); align-items: center; padding: clamp(22px, 3.5vw, 40px); margin-top: clamp(16px, 3vw, 28px); }-- .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; }++ .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 10px; }++ .arch__copy h3 { margin-top: 0; }.arch__copy h3 { font-size: clamp(1.3rem, 2.2vw, 1.8rem); margin-bottom: .6em; }.arch__facts { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }.arch__facts li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); font-size: .9rem; }@@@ -162,4 +169,36 @@ const edges = [.arch__node:hover .arch__node-box { stroke: var(--accent); }.arch__node-t { font: 600 13px var(--font-body); fill: var(--fg); }@media (max-width: 900px) { .arch { grid-template-columns: 1fr; } }++++ /* Phones: the network as a compact grid (Live across the top, the other six two by two: icon,++ short name, stat); the explainer paragraph folds behind "Read more". */++ .arch__more summary { display: none; }++ @media (max-width: 560px) {++ .bento { grid-template-columns: 1fr 1fr; grid-auto-rows: auto; gap: 8px; }++ .tile, .tile--big, .tile--community, .tile--sites { grid-column: span 1; min-height: 0; }++ .tile--big { grid-column: 1 / -1; }++ .tile__pre { display: none; }++ .tile__body { padding: 12px 12px 11px; gap: 4px; }++ .tile__top { margin-bottom: 2px; gap: 8px; }++ .tile__icon { width: 28px; height: 28px; border-radius: 8px; font-size: .78rem; }++ .tile__role { font-size: .6rem; letter-spacing: .08em; }++ .tile__name, .tile--big .tile__name { font-size: 1.05rem; }++ .tile--big .tile__name { font-size: 1.35rem; }++ .tile__blurb { display: none; }++ .tile--big .tile__blurb { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: .85rem; }++ .tile__foot { margin-top: 2px; }++ .tile__stat { white-space: normal; line-height: 1.35; }++ .tile__go { display: none; }++ .tile__shot img { opacity: .18; }++ .arch__more summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; margin: 4px 0 10px; font: 600 .82rem var(--font-mono); color: var(--accent); }++ .arch__more summary::-webkit-details-marker { display: none; }++ .arch__more summary::after { content: "▾"; transition: transform .2s; }++ .arch__more[open] summary::after { transform: rotate(180deg); }++ .arch__more[open] summary { margin-bottom: 4px; }++ }</style>++++<script>++ // Phones start with the architecture explainer folded; wider screens (and no-JS) show it open.++ if (matchMedia('(max-width: 560px)').matches) document.querySelectorAll<HTMLDetailsElement>('.arch__more').forEach((d) => { d.open = false; });++</script>@@@ -86,7 +86,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));position: relative; overflow: hidden; display: flex; flex-direction: column;border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border);transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease), opacity .4s var(--ease);-- will-change: transform;}.pcard:hover { border-color: rgba(var(--accent-rgb), .4); box-shadow: var(--shadow-card), var(--shadow-glow); }.pcard.is-hidden { display: none; }@@@ -120,6 +119,9 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }++ /* Phones: a card without a screenshot drops the icon-only placeholder (its icon is already in the++ card's top row), and screenshots are shorter, so the list is not mostly empty frames. */++ @media (max-width: 620px) { .pcard__deco { display: none; } .pcard__img { aspect-ratio: 16 / 8; } }</style><script>@@@ -41,8 +41,9 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;</section><style>-- .sk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }-- .sk { padding: 22px; }++ /* Columns, not a grid: cards of different lengths stack without blank space under the shorter ones. */++ .sk-grid { columns: 3; column-gap: 14px; }++ .sk { padding: 22px; break-inside: avoid; margin-bottom: 14px; }.sk__head { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 14px; }.sk__icon { width: 40px; height: 40px; flex: none; border-radius: 12px; display: inline-grid; place-items: center; background: var(--grad-soft); color: var(--fg); border: 1px solid var(--border); font-size: .95rem; }.sk__title { margin: 0 0 3px; font-size: 1.05rem; }@@@ -55,7 +56,6 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;.sk__bars i { width: 12px; height: 5px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--border); transition: background .3s, box-shadow .3s; }.sk__bars i.on { background: var(--accent); border-color: transparent; box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); }.sk__item:hover .sk__bars i.on { background: var(--accent-2); box-shadow: 0 0 8px rgba(var(--accent-2-rgb), .5); }-- @media (min-width: 1001px) { .sk:last-child { grid-column: 1 / -1; } .sk:last-child .sk__list { grid-template-columns: repeat(3, 1fr); column-gap: 28px; } .sk:last-child .sk__item:nth-child(-n+3) { border-top: 0; } }-- @media (max-width: 1000px) { .sk-grid { grid-template-columns: repeat(2, 1fr); } }-- @media (max-width: 620px) { .sk-grid { grid-template-columns: 1fr; } }++ @media (max-width: 1000px) { .sk-grid { columns: 2; } }++ @media (max-width: 620px) { .sk-grid { columns: 1; } .sk { padding: 18px; } }</style>@@@ -30,7 +30,6 @@ const kindIcon: Record<string, string> = {<article class="tl__card card spot"><header class="tl__head"><div class="tl__when mono">-- <i class={`${kindIcon[e.data.kind]} tl__kind`} aria-hidden="true"></i>{e.data.current && <span class="live-dot"></span>}<span>{e.data.start} — {e.data.end}</span></div>@@@ -95,12 +94,13 @@ const kindIcon: Record<string, string> = {.tl__item.is-featured .tl__card { padding: clamp(24px, 3vw, 34px); }}.tl__aside { display: none; }-- .tl__kind { display: none; }-- /* Phones and tablets: no separate rail or nodes eating the left edge. The card uses the full width,-- the progress line is built into its left side (filling as you scroll through it) and the job-type-- icon sits with the dates. */++ /* Phones and tablets: no separate rail eating the left edge. The card uses the full width, the++ progress line is built into its left side (filling as you scroll through it), and each job's icon++ node sits on that line beside the title, half in the page gutter, so it costs the text no width. */@media (max-width: 899px) {-- .tl__rail, .tl__node { display: none; }++ .tl__rail { display: none; }++ .tl__node { left: -14px; top: 22px; width: 32px; height: 32px; font-size: .72rem; z-index: 1; }++ .tl__head { padding-left: 18px; }.tl__item { padding-left: 0; }.tl__card { overflow: hidden; }.tl__card::after {@@@ -108,7 +108,6 @@ const kindIcon: Record<string, string> = {background: linear-gradient(180deg, var(--accent), var(--accent-2)) top / 100% calc(var(--p, 0) * 100%) no-repeat, var(--border-strong);}.tl__item.is-current .tl__card::after { box-shadow: 0 0 12px rgba(var(--accent-rgb), .45); }-- .tl__kind { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; font-size: .68rem; letter-spacing: 0; background: rgba(var(--accent-rgb), .12); color: var(--accent); }}@media (min-width: 900px) { .tl__aside { display: grid; } }.tl__aside-when { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); display: inline-flex; align-items: center; gap: 10px; }@@@ -125,14 +125,17 @@ const groups = [/* Each group carries its own tone so the eye can tell them apart mid-scroll: a colored iconsquare, a matching left rule and a faint tinted background. */.wk-group { --g: var(--accent); --g-rgb: var(--accent-rgb); position: relative; padding: clamp(16px, 2.4vw, 24px); border-radius: var(--radius-lg); background: linear-gradient(135deg, rgba(var(--g-rgb), .07), transparent 55%), var(--surface); border: 1px solid var(--border); scroll-margin-top: 90px; }-- .wk-group::before { content: ""; position: absolute; left: -1px; top: 18%; bottom: 18%; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }++ /* The group's accent rule sits on its left edge beside the title only (not the length of the group). */++ .wk-group__ico { position: relative; }++ .wk-group__ico::before { content: ""; position: absolute; left: calc(-1 * clamp(16px, 2.4vw, 24px) - 2px); top: -2px; bottom: -2px; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }.wk-group--b { --g: var(--accent-2); --g-rgb: var(--accent-2-rgb); }.wk-group--c { --g: var(--accent-3); --g-rgb: var(--accent-3-rgb); }.wk-group--d { --g: var(--violet); --g-rgb: 139, 92, 246; }.wk-group__head { display: grid; grid-template-columns: auto 1fr auto; gap: 14px 16px; align-items: center; margin-bottom: 16px; }.wk-group__ico { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); font-size: 1.05rem; box-shadow: 0 10px 24px -14px rgba(var(--g-rgb), .9); }.wk-group__txt { min-width: 0; }-- .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); display: flex; align-items: center; gap: 10px; }++ .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); }++ .wk-group__title .wk-group__n { display: inline-block; margin-left: 8px; vertical-align: .15em; line-height: 1.4; }.wk-group__n { font-size: .72rem; padding: 2px 8px; border-radius: 999px; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); }.wk-group__blurb { margin: 3px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.5; }.wk-group__all { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(var(--g-rgb), .35); background: rgba(var(--g-rgb), .08); color: var(--fg); font: 600 .8rem var(--font-body); text-decoration: none; white-space: nowrap; transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease); }@@@ -141,7 +144,7 @@ const groups = [.wk-group__all:hover { text-decoration: none; background: rgba(var(--g-rgb), .16); border-color: rgba(var(--g-rgb), .6); transform: translateY(-1px); }.wk-group__all:hover .wk-group__arrow { transform: translateX(3px); }-- .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }++ .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }.wk-card { min-width: 0; }.wk-card__link { display: flex; flex-direction: column; height: 100%; border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--border); color: var(--fg); text-decoration: none; transition: transform .4s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease); }.wk-card__link:hover { text-decoration: none; transform: translateY(-3px); border-color: rgba(var(--g-rgb), .5); background: var(--surface-2); box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--g-rgb), .12), 0 16px 40px -22px rgba(var(--g-rgb), .7); }@@@ -188,7 +191,6 @@ const groups = [.work__all-go { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08); transition: transform .4s var(--ease), background .3s; }.work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }-- @media (max-width: 1100px) { .wk-grid { grid-template-columns: repeat(3, 1fr); } }-- @media (max-width: 820px) { .wk-grid { grid-template-columns: repeat(2, 1fr); } .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }++ @media (max-width: 820px) { .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }@media (max-width: 480px) { .wk-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .wk-card__body { padding: 10px 11px; } .wk-card__text { display: none; } .wk-card__title { font-size: .9rem; } .wk-card--img .wk-card__meta { display: none; } .work__all-btn { padding: 18px; gap: 14px; } .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; } }</style>@@@ -153,7 +153,7 @@ const imageUrl = new URL(image, site).toString();// Text on top of the accent: relative luminance decides dark ink vs white.var rgb = hexToRgbStr(accent).split(', ').map(function (v) { v = parseInt(v, 10) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });var lum = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];-- root.setProperty('--on-accent', lum > 0.36 ? '#0b0f1a' : '#ffffff');++ root.setProperty('--on-accent', lum > 0.5 ? '#0b0f1a' : '#ffffff');root.setProperty('--accent', accent);root.setProperty('--accent-rgb', hexToRgbStr(accent));root.setProperty('--violet', violet);@@@ -85,8 +85,8 @@ const catLabel: Record<string, string> = {.pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }-- .pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }-- .pp__meta-wide { grid-column: 1 / -1; }++ .pp__meta { display: flex; flex-wrap: wrap; gap: 14px 36px; margin: 1.4rem 0; }++ .pp__meta-wide { flex-basis: 100%; }.pp__meta strong { display: block; color: var(--accent); font-family: var(--font-mono); font-size: .72rem; text-transform: uppercase; letter-spacing: .14em; margin-bottom: 6px; }.pp__meta span { color: var(--muted); font-size: .94rem; }.pp__tech { display: flex; flex-wrap: wrap; gap: 6px; }@@@ -105,5 +105,6 @@ const catLabel: Record<string, string> = {.pp__nav-link .mono { font-size: .72rem; color: var(--dim); letter-spacing: .1em; text-transform: uppercase; }.pp__nav-link:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateY(-3px); text-decoration: none; }.pp__nav-link--next { text-align: right; }++ @media (max-width: 560px) { .pp { padding-bottom: 48px; } .pp__prose { padding: 18px; } }@media (max-width: 820px) { .pp__body { grid-template-columns: 1fr; } .pp__side { position: static; } .pp__nav { grid-template-columns: 1fr; } .pp__nav-link--next { text-align: left; } }</style>@@@ -116,6 +116,7 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);.resume__contact a:hover { color: var(--accent); text-decoration: none; }.resume__contact i { color: var(--accent); width: 14px; }.resume__actions { display: flex; gap: 10px; flex-wrap: wrap; padding: 22px 0 6px; }++ @media (max-width: 560px) { .resume__actions { display: grid; grid-template-columns: 1fr; } .resume__actions > .btn { justify-content: center; } }.resume__pdf-note { font-size: .82rem; color: var(--dim); max-width: 62ch; margin: 0 0 8px; }.resume__pdf-note strong { color: var(--muted); }.themed-pdf-caret { font-size: .68em; transition: transform .3s var(--ease); }@@@ -55,6 +55,22 @@ body::after {animation: grain 1.4s steps(4) infinite;}:root[data-theme="light"] body::after { opacity: .035; }++/* Phones: the grid was so faint (and masked so tight) that only the jittering grain showed, which++ read as the screen glitching. Here the grid is clearly there over more of the screen, and the++ grain is a slower, lighter texture. */++@media (max-width: 900px) {++ body::before {++ background-image:++ linear-gradient(to right, rgba(160,180,255,.085) 1px, transparent 1px),++ linear-gradient(to bottom, rgba(160,180,255,.085) 1px, transparent 1px);++ background-size: 48px 48px;++ mask-image: linear-gradient(to bottom, transparent 0, transparent 96px, #000 170px),++ radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);++ -webkit-mask-image: -webkit-linear-gradient(top, transparent 0, transparent 96px, #000 170px),++ -webkit-radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);++ }++ body::after { opacity: .035; animation-duration: 4s; }++}@keyframes grain {0% { transform: translate(0, 0); } 25% { transform: translate(-3%, 2%); }50% { transform: translate(2%, -3%); } 75% { transform: translate(-1%, -1%); } 100% { transform: translate(0, 0); }@@@ -95,7 +111,7 @@ p { line-height: 1.7; color: var(--muted); margin: 0 0 1em; }strong { color: var(--fg); font-weight: 600; }.container { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }--.section { padding: clamp(52px, 7.5vw, 116px) 0; position: relative; }++.section { padding: clamp(36px, 6vw, 88px) 0; position: relative; }/* A hairline between sections plus a faint alternating band so sections still read as distinctblocks when someone is flicking through the page fast. */.section + .section::before, main > .section:not(.hero):first-child::before {@@@ -142,6 +158,9 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpartransition: transform .5s var(--ease), border-color .4s var(--ease), box-shadow .5s var(--ease), background .4s var(--ease);}.card:hover { border-color: rgba(var(--accent-rgb), .35); box-shadow: var(--shadow-card), var(--shadow-glow); }++/* Phones: no backdrop blur on cards (little to see through on a flat background, and every blurred++ card is another layer the phone must composite; dropped layers painted as blank boxes). */++@media (max-width: 900px) { .card { -webkit-backdrop-filter: none; backdrop-filter: none; } }/* Pointer-tracked spotlight (set --mx/--my in JS) */.spot { --mx: 50%; --my: 50%; }@@@ -163,7 +182,6 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transparfont: 600 .98rem/1 var(--font-body); letter-spacing: .005em;cursor: pointer; text-decoration: none; white-space: nowrap;transition: transform .35s var(--ease), border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease), box-shadow .35s var(--ease);-- will-change: transform;}.btn:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .6); background: rgba(var(--accent-rgb), .1); transform: translateY(-1px); }.btn i { font-size: .95em; }@@@ -172,6 +190,7 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transparhighlight and inner ring keep it from reading as one flat slab of color. */.btn--primary {--btn-bg: var(--accent); color: var(--on-accent); border-color: transparent; overflow: hidden;++ text-shadow: 0 1px 1px rgba(0, 0, 0, .18);background-image: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 60%);box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 0 0 1px rgba(0,0,0,.08), 0 12px 30px -12px rgba(var(--accent-rgb), .75);}@@@ -22,7 +22,7 @@--accent-3: #3ee6b0;--accent-3-rgb: 62, 230, 176;--violet: #8b5cf6;-- --on-accent: #06091a;++ --on-accent: #ffffff;--grad: linear-gradient(95deg, var(--accent) 0%, var(--violet) 48%, var(--accent-2) 100%);--grad-soft: linear-gradient(135deg, rgba(var(--accent-rgb), .22), rgba(139, 92, 246, .18) 50%, rgba(var(--accent-2-rgb), .16));
@@ -28,7 +28,8 @@ import profile from "../data/profile.json";
@@ -28,7 +28,8 @@ import profile from "../data/profile.json";
</section>
</section>
<style>
<style>
- .ct { overflow: hidden; padding: clamp(28px, 5vw, 60px); }
+ .ct { overflow: hidden; padding: clamp(20px, 4vw, 48px); }
+ @media (max-width: 560px) { .ct__actions { display: grid; grid-template-columns: 1fr; } .ct__actions .btn { justify-content: center; } }
.ct__glow { position: absolute; inset: -40% -20% auto auto; width: 60%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(closest-side, rgba(var(--accent-rgb), .35), transparent 70%); filter: blur(30px); animation: breathe 9s ease-in-out infinite; pointer-events: none; }
.ct__glow { position: absolute; inset: -40% -20% auto auto; width: 60%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(closest-side, rgba(var(--accent-rgb), .35), transparent 70%); filter: blur(30px); animation: breathe 9s ease-in-out infinite; pointer-events: none; }
@keyframes breathe { 50% { transform: scale(1.15) translate(-6%, 8%); opacity: .7; } }
@keyframes breathe { 50% { transform: scale(1.15) translate(-6%, 8%); opacity: .7; } }
.ct__grid { position: relative; display: grid; grid-template-columns: 1.3fr 1fr; gap: clamp(24px, 4vw, 56px); align-items: center; }
.ct__grid { position: relative; display: grid; grid-template-columns: 1.3fr 1fr; gap: clamp(24px, 4vw, 56px); align-items: center; }
@@ -24,11 +24,6 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
@@ -24,11 +24,6 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
<div class="now__grid">
<div class="now__grid">
<article class="card spot now__pitch" data-reveal>
<article class="card spot now__pitch" data-reveal>
<p class="now__text">{pitchParts.map((p) => typeof p === "string" ? p : <a href={p.href} target={p.external ? "_blank" : undefined} rel={p.external ? "noopener" : undefined}>{p.text}</a>)}</p>
<p class="now__text">{pitchParts.map((p) => typeof p === "string" ? p : <a href={p.href} target={p.external ? "_blank" : undefined} rel={p.external ? "noopener" : undefined}>{p.text}</a>)}</p>
- <ul class="now__hl" aria-label="What I bring">
- {profile.highlights.map((h) => (
- <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>
- ))}
- </ul>
<p class="now__text">Ten years of running my own company taught me to care about the whole loop: the code, the servers, the invoice and the person on the other end. I like small teams, hard problems and software that stays up.</p>
<p class="now__text">Ten years of running my own company taught me to care about the whole loop: the code, the servers, the invoice and the person on the other end. I like small teams, hard problems and software that stays up.</p>
<div class="now__cta">
<div class="now__cta">
<a href="#experience" class="btn btn--ghost">Experience <span class="btn__arrow">→</span></a>
<a href="#experience" class="btn btn--ghost">Experience <span class="btn__arrow">→</span></a>
@@ -36,6 +31,12 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
@@ -36,6 +31,12 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
</div>
</div>
</article>
</article>
+ <ul class="now__hl" aria-label="What I bring" data-stagger>
+ {profile.highlights.map((h) => (
+ <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>
+ ))}
+ </ul>
+
<aside class="now__side">
<aside class="now__side">
<div class="card spot now__status" data-reveal data-delay="100">
<div class="card spot now__status" data-reveal data-delay="100">
<div class="now__status-head"><span class="live-dot"></span><span class="eyebrow">Right now</span></div>
<div class="now__status-head"><span class="live-dot"></span><span class="eyebrow">Right now</span></div>
@@ -61,17 +62,22 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
@@ -61,17 +62,22 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
</section>
</section>
<style>
<style>
- .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 18px; align-items: start; }
+ /* The pitch and the side cards share the first row; the five strengths run under both as their own
+ row, so neither column ends far above the other. Phones: pitch, strengths, then the side cards. */
+ .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; grid-template-areas: "pitch side" "hl hl"; gap: 18px; align-items: start; }
+ .now__pitch { grid-area: pitch; } .now__side { grid-area: side; } .now__hl { grid-area: hl; }
.now__pitch { padding: clamp(24px, 3.5vw, 40px); }
.now__pitch { padding: clamp(24px, 3.5vw, 40px); }
.now__text { font-size: 1.08rem; line-height: 1.8; color: var(--muted); }
.now__text { font-size: 1.08rem; line-height: 1.8; color: var(--muted); }
.now__text:first-child { color: var(--fg); font-size: clamp(1.1rem, 1.6vw, 1.3rem); font-weight: 500; line-height: 1.6; letter-spacing: -.005em; }
.now__text:first-child { color: var(--fg); font-size: clamp(1.1rem, 1.6vw, 1.3rem); font-weight: 500; line-height: 1.6; letter-spacing: -.005em; }
- .now__hl { list-style: none; margin: 22px 0 18px; padding: 0; display: grid; gap: 10px; }
+ .now__hl { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }
.now__hl li { display: flex; gap: 14px; align-items: flex-start; padding: 12px 14px; border-radius: 14px; background: var(--surface); border: 1px solid var(--border); transition: border-color .3s var(--ease), transform .3s var(--ease); }
.now__hl li { display: flex; gap: 14px; align-items: flex-start; padding: 12px 14px; border-radius: 14px; background: var(--surface); border: 1px solid var(--border); transition: border-color .3s var(--ease), transform .3s var(--ease); }
+ @media (min-width: 901px) { .now__hl li { flex-direction: column; gap: 10px; } }
.now__hl li:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateX(4px); }
.now__hl li:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateX(4px); }
.now__hl i { flex: none; width: 36px; height: 36px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .12); color: var(--accent); font-size: .95rem; }
.now__hl i { flex: none; width: 36px; height: 36px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .12); color: var(--accent); font-size: .95rem; }
.now__hl strong { display: block; color: var(--fg); font-size: .98rem; margin-bottom: 2px; }
.now__hl strong { display: block; color: var(--fg); font-size: .98rem; margin-bottom: 2px; }
.now__hl span { color: var(--muted); font-size: .9rem; line-height: 1.55; }
.now__hl span { color: var(--muted); font-size: .9rem; line-height: 1.55; }
- .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
+ .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
+ @media (max-width: 560px) { .now__cta { display: grid; grid-template-columns: 1fr 1fr; } .now__cta .btn { justify-content: center; padding: 12px 14px; } .now__pitch { padding: 20px; } }
.now__side { display: grid; gap: 18px; }
.now__side { display: grid; gap: 18px; }
.now__status-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.now__status-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.now__status-list, .now__seek { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.now__status-list, .now__seek { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
@@ -86,5 +92,5 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
@@ -86,5 +92,5 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
.now__seek li:first-child { border-top: 0; padding-top: 0; }
.now__seek li:first-child { border-top: 0; padding-top: 0; }
.now__seek strong { color: var(--fg); font-size: .95rem; }
.now__seek strong { color: var(--fg); font-size: .95rem; }
.now__seek span { color: var(--muted); font-size: .86rem; }
.now__seek span { color: var(--muted); font-size: .86rem; }
- @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; } }
+ @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; grid-template-areas: "pitch" "hl" "side"; } }
</style>
</style>
@@ -7,6 +7,8 @@ const icons: Record<string, string> = {
@@ -7,6 +7,8 @@ const icons: Record<string, string> = {
live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film",
live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film",
tools: "fa-solid fa-toolbox", games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",
tools: "fa-solid fa-toolbox", games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",
};
};
+// "OpenVibe.Live" -> "OpenVibe." + "Live": phones show only the short name (the group already says OpenVibe).
+const short = (n: string) => n.startsWith("OpenVibe.") ? n.slice(9) : null;
// Architecture diagram geometry (viewBox 800 x 420)
// Architecture diagram geometry (viewBox 800 x 420)
const hub = { x: 400, y: 210 };
const hub = { x: 400, y: 210 };
const nodes = [
const nodes = [
@@ -34,7 +36,7 @@ const edges = [
@@ -34,7 +36,7 @@ const edges = [
<span class="tile__icon"><i class={icons[big.key]}></i></span>
<span class="tile__icon"><i class={icons[big.key]}></i></span>
<span class="tile__role">{big.role}</span>
<span class="tile__role">{big.role}</span>
</div>
</div>
- <h3 class="tile__name">{big.name}</h3>
+ <h3 class="tile__name">{short(big.name) ? <><span class="tile__pre">OpenVibe.</span>{short(big.name)}</> : big.name}</h3>
<p class="tile__blurb">{big.blurb}</p>
<p class="tile__blurb">{big.blurb}</p>
<div class="tile__foot"><span class="tile__stat mono">{big.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
<div class="tile__foot"><span class="tile__stat mono">{big.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
</div>
</div>
@@ -48,7 +50,7 @@ const edges = [
@@ -48,7 +50,7 @@ const edges = [
<span class="tile__icon"><i class={icons[s.key]}></i></span>
<span class="tile__icon"><i class={icons[s.key]}></i></span>
<span class="tile__role">{s.role}</span>
<span class="tile__role">{s.role}</span>
</div>
</div>
- <h3 class="tile__name">{s.name}</h3>
+ <h3 class="tile__name">{short(s.name) ? <><span class="tile__pre">OpenVibe.</span>{short(s.name)}</> : s.name}</h3>
<p class="tile__blurb">{s.blurb}</p>
<p class="tile__blurb">{s.blurb}</p>
<div class="tile__foot"><span class="tile__stat mono">{s.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
<div class="tile__foot"><span class="tile__stat mono">{s.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
</div>
</div>
@@ -60,7 +62,10 @@ const edges = [
@@ -60,7 +62,10 @@ const edges = [
<div class="arch__copy">
<div class="arch__copy">
<div class="eyebrow"><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> How it fits together</div>
<div class="eyebrow"><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> How it fits together</div>
<h3>One login, one wallet, seven products that feel like one.</h3>
<h3>One login, one wallet, seven products that feel like one.</h3>
+ <details class="arch__more" open>
+ <summary>Read more</summary>
<p>Sign in once and every OpenVibe site knows who you are, what you own and what you look like. The identity service in the middle handles accounts, security and the shared virtual currency; every other product plugs into it through clear, documented APIs. That's the same playbook I bring to any company: secure single sign-on, payments and APIs designed so new products can be added without breaking the old ones.</p>
<p>Sign in once and every OpenVibe site knows who you are, what you own and what you look like. The identity service in the middle handles accounts, security and the shared virtual currency; every other product plugs into it through clear, documented APIs. That's the same playbook I bring to any company: secure single sign-on, payments and APIs designed so new products can be added without breaking the old ones.</p>
+ </details>
<ul class="arch__facts">
<ul class="arch__facts">
<li><i class="fa-solid fa-rotate"></i> Updates ship with zero downtime, so users never see an outage</li>
<li><i class="fa-solid fa-rotate"></i> Updates ship with zero downtime, so users never see an outage</li>
<li><i class="fa-solid fa-cloud-arrow-up"></i> Media storage tiers automatically from fast local disks to cheap cloud storage</li>
<li><i class="fa-solid fa-cloud-arrow-up"></i> Media storage tiers automatically from fast local disks to cheap cloud storage</li>
@@ -107,7 +112,6 @@ const edges = [
@@ -107,7 +112,6 @@ const edges = [
min-height: 210px; border-radius: var(--radius-lg); text-decoration: none; color: var(--fg);
min-height: 210px; border-radius: var(--radius-lg); text-decoration: none; color: var(--fg);
background: var(--surface); border: 1px solid var(--border);
background: var(--surface); border: 1px solid var(--border);
transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease);
transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease);
- will-change: transform;
}
}
.tile:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .45); box-shadow: var(--shadow-card), var(--shadow-glow); }
.tile:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .45); box-shadow: var(--shadow-card), var(--shadow-glow); }
.tile--big { grid-column: span 2; grid-row: span 2; min-height: 440px; }
.tile--big { grid-column: span 2; grid-row: span 2; min-height: 440px; }
@@ -131,11 +135,14 @@ const edges = [
@@ -131,11 +135,14 @@ const edges = [
.tile--sites { background: var(--grad-soft); }
.tile--sites { background: var(--grad-soft); }
.tile--sites::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 60% at 80% 20%, rgba(var(--accent-2-rgb), .25), transparent 70%); }
.tile--sites::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 60% at 80% 20%, rgba(var(--accent-2-rgb), .25), transparent 70%); }
@media (max-width: 1024px) { .bento { grid-template-columns: repeat(2, 1fr); } .tile--big { grid-column: span 2; grid-row: span 1; min-height: 360px; } .tile--community, .tile--sites { grid-column: span 1; } }
@media (max-width: 1024px) { .bento { grid-template-columns: repeat(2, 1fr); } .tile--big { grid-column: span 2; grid-row: span 1; min-height: 360px; } .tile--community, .tile--sites { grid-column: span 1; } }
+ /* The diagram is unreadable at phone width; the facts beside it say the same thing. */
+ @media (max-width: 560px) { .arch__svg { display: none; } .arch { padding: 18px; } }
@media (max-width: 560px) { .bento { grid-template-columns: 1fr; } .tile--big, .tile--community, .tile--sites { grid-column: span 1; } .tile--big { min-height: 320px; } }
@media (max-width: 560px) { .bento { grid-template-columns: 1fr; } .tile--big, .tile--community, .tile--sites { grid-column: span 1; } .tile--big { min-height: 320px; } }
/* Architecture */
/* Architecture */
.arch { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(20px, 4vw, 48px); align-items: center; padding: clamp(22px, 3.5vw, 40px); margin-top: clamp(16px, 3vw, 28px); }
.arch { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(20px, 4vw, 48px); align-items: center; padding: clamp(22px, 3.5vw, 40px); margin-top: clamp(16px, 3vw, 28px); }
- .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; }
+ .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 10px; }
+ .arch__copy h3 { margin-top: 0; }
.arch__copy h3 { font-size: clamp(1.3rem, 2.2vw, 1.8rem); margin-bottom: .6em; }
.arch__copy h3 { font-size: clamp(1.3rem, 2.2vw, 1.8rem); margin-bottom: .6em; }
.arch__facts { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.arch__facts { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.arch__facts li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); font-size: .9rem; }
.arch__facts li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); font-size: .9rem; }
@@ -162,4 +169,36 @@ const edges = [
@@ -162,4 +169,36 @@ const edges = [
.arch__node:hover .arch__node-box { stroke: var(--accent); }
.arch__node:hover .arch__node-box { stroke: var(--accent); }
.arch__node-t { font: 600 13px var(--font-body); fill: var(--fg); }
.arch__node-t { font: 600 13px var(--font-body); fill: var(--fg); }
@media (max-width: 900px) { .arch { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .arch { grid-template-columns: 1fr; } }
+
+ /* Phones: the network as a compact grid (Live across the top, the other six two by two: icon,
+ short name, stat); the explainer paragraph folds behind "Read more". */
+ .arch__more summary { display: none; }
+ @media (max-width: 560px) {
+ .bento { grid-template-columns: 1fr 1fr; grid-auto-rows: auto; gap: 8px; }
+ .tile, .tile--big, .tile--community, .tile--sites { grid-column: span 1; min-height: 0; }
+ .tile--big { grid-column: 1 / -1; }
+ .tile__pre { display: none; }
+ .tile__body { padding: 12px 12px 11px; gap: 4px; }
+ .tile__top { margin-bottom: 2px; gap: 8px; }
+ .tile__icon { width: 28px; height: 28px; border-radius: 8px; font-size: .78rem; }
+ .tile__role { font-size: .6rem; letter-spacing: .08em; }
+ .tile__name, .tile--big .tile__name { font-size: 1.05rem; }
+ .tile--big .tile__name { font-size: 1.35rem; }
+ .tile__blurb { display: none; }
+ .tile--big .tile__blurb { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: .85rem; }
+ .tile__foot { margin-top: 2px; }
+ .tile__stat { white-space: normal; line-height: 1.35; }
+ .tile__go { display: none; }
+ .tile__shot img { opacity: .18; }
+ .arch__more summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; margin: 4px 0 10px; font: 600 .82rem var(--font-mono); color: var(--accent); }
+ .arch__more summary::-webkit-details-marker { display: none; }
+ .arch__more summary::after { content: "▾"; transition: transform .2s; }
+ .arch__more[open] summary::after { transform: rotate(180deg); }
+ .arch__more[open] summary { margin-bottom: 4px; }
+ }
</style>
</style>
+
+<script>
+ // Phones start with the architecture explainer folded; wider screens (and no-JS) show it open.
+ if (matchMedia('(max-width: 560px)').matches) document.querySelectorAll<HTMLDetailsElement>('.arch__more').forEach((d) => { d.open = false; });
+</script>
@@ -86,7 +86,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -86,7 +86,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
position: relative; overflow: hidden; display: flex; flex-direction: column;
position: relative; overflow: hidden; display: flex; flex-direction: column;
border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border);
transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease), opacity .4s var(--ease);
transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease), opacity .4s var(--ease);
- will-change: transform;
}
}
.pcard:hover { border-color: rgba(var(--accent-rgb), .4); box-shadow: var(--shadow-card), var(--shadow-glow); }
.pcard:hover { border-color: rgba(var(--accent-rgb), .4); box-shadow: var(--shadow-card), var(--shadow-glow); }
.pcard.is-hidden { display: none; }
.pcard.is-hidden { display: none; }
@@ -120,6 +119,9 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -120,6 +119,9 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }
.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }
@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }
+ /* Phones: a card without a screenshot drops the icon-only placeholder (its icon is already in the
+ card's top row), and screenshots are shorter, so the list is not mostly empty frames. */
+ @media (max-width: 620px) { .pcard__deco { display: none; } .pcard__img { aspect-ratio: 16 / 8; } }
</style>
</style>
<script>
<script>
@@ -41,8 +41,9 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
@@ -41,8 +41,9 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
</section>
</section>
<style>
<style>
- .sk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
- .sk { padding: 22px; }
+ /* Columns, not a grid: cards of different lengths stack without blank space under the shorter ones. */
+ .sk-grid { columns: 3; column-gap: 14px; }
+ .sk { padding: 22px; break-inside: avoid; margin-bottom: 14px; }
.sk__head { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 14px; }
.sk__head { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 14px; }
.sk__icon { width: 40px; height: 40px; flex: none; border-radius: 12px; display: inline-grid; place-items: center; background: var(--grad-soft); color: var(--fg); border: 1px solid var(--border); font-size: .95rem; }
.sk__icon { width: 40px; height: 40px; flex: none; border-radius: 12px; display: inline-grid; place-items: center; background: var(--grad-soft); color: var(--fg); border: 1px solid var(--border); font-size: .95rem; }
.sk__title { margin: 0 0 3px; font-size: 1.05rem; }
.sk__title { margin: 0 0 3px; font-size: 1.05rem; }
@@ -55,7 +56,6 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
@@ -55,7 +56,6 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
.sk__bars i { width: 12px; height: 5px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--border); transition: background .3s, box-shadow .3s; }
.sk__bars i { width: 12px; height: 5px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--border); transition: background .3s, box-shadow .3s; }
.sk__bars i.on { background: var(--accent); border-color: transparent; box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); }
.sk__bars i.on { background: var(--accent); border-color: transparent; box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); }
.sk__item:hover .sk__bars i.on { background: var(--accent-2); box-shadow: 0 0 8px rgba(var(--accent-2-rgb), .5); }
.sk__item:hover .sk__bars i.on { background: var(--accent-2); box-shadow: 0 0 8px rgba(var(--accent-2-rgb), .5); }
- @media (min-width: 1001px) { .sk:last-child { grid-column: 1 / -1; } .sk:last-child .sk__list { grid-template-columns: repeat(3, 1fr); column-gap: 28px; } .sk:last-child .sk__item:nth-child(-n+3) { border-top: 0; } }
- @media (max-width: 1000px) { .sk-grid { grid-template-columns: repeat(2, 1fr); } }
- @media (max-width: 620px) { .sk-grid { grid-template-columns: 1fr; } }
+ @media (max-width: 1000px) { .sk-grid { columns: 2; } }
+ @media (max-width: 620px) { .sk-grid { columns: 1; } .sk { padding: 18px; } }
</style>
</style>
@@ -30,7 +30,6 @@ const kindIcon: Record<string, string> = {
@@ -30,7 +30,6 @@ const kindIcon: Record<string, string> = {
<article class="tl__card card spot">
<article class="tl__card card spot">
<header class="tl__head">
<header class="tl__head">
<div class="tl__when mono">
<div class="tl__when mono">
- <i class={`${kindIcon[e.data.kind]} tl__kind`} aria-hidden="true"></i>
{e.data.current && <span class="live-dot"></span>}
{e.data.current && <span class="live-dot"></span>}
<span>{e.data.start} — {e.data.end}</span>
<span>{e.data.start} — {e.data.end}</span>
</div>
</div>
@@ -95,12 +94,13 @@ const kindIcon: Record<string, string> = {
@@ -95,12 +94,13 @@ const kindIcon: Record<string, string> = {
.tl__item.is-featured .tl__card { padding: clamp(24px, 3vw, 34px); }
.tl__item.is-featured .tl__card { padding: clamp(24px, 3vw, 34px); }
}
}
.tl__aside { display: none; }
.tl__aside { display: none; }
- .tl__kind { display: none; }
- /* Phones and tablets: no separate rail or nodes eating the left edge. The card uses the full width,
- the progress line is built into its left side (filling as you scroll through it) and the job-type
- icon sits with the dates. */
+ /* Phones and tablets: no separate rail eating the left edge. The card uses the full width, the
+ progress line is built into its left side (filling as you scroll through it), and each job's icon
+ node sits on that line beside the title, half in the page gutter, so it costs the text no width. */
@media (max-width: 899px) {
@media (max-width: 899px) {
- .tl__rail, .tl__node { display: none; }
+ .tl__rail { display: none; }
+ .tl__node { left: -14px; top: 22px; width: 32px; height: 32px; font-size: .72rem; z-index: 1; }
+ .tl__head { padding-left: 18px; }
.tl__item { padding-left: 0; }
.tl__item { padding-left: 0; }
.tl__card { overflow: hidden; }
.tl__card { overflow: hidden; }
.tl__card::after {
.tl__card::after {
@@ -108,7 +108,6 @@ const kindIcon: Record<string, string> = {
@@ -108,7 +108,6 @@ const kindIcon: Record<string, string> = {
background: linear-gradient(180deg, var(--accent), var(--accent-2)) top / 100% calc(var(--p, 0) * 100%) no-repeat, var(--border-strong);
background: linear-gradient(180deg, var(--accent), var(--accent-2)) top / 100% calc(var(--p, 0) * 100%) no-repeat, var(--border-strong);
}
}
.tl__item.is-current .tl__card::after { box-shadow: 0 0 12px rgba(var(--accent-rgb), .45); }
.tl__item.is-current .tl__card::after { box-shadow: 0 0 12px rgba(var(--accent-rgb), .45); }
- .tl__kind { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; font-size: .68rem; letter-spacing: 0; background: rgba(var(--accent-rgb), .12); color: var(--accent); }
}
}
@media (min-width: 900px) { .tl__aside { display: grid; } }
@media (min-width: 900px) { .tl__aside { display: grid; } }
.tl__aside-when { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); display: inline-flex; align-items: center; gap: 10px; }
.tl__aside-when { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); display: inline-flex; align-items: center; gap: 10px; }
@@ -125,14 +125,17 @@ const groups = [
@@ -125,14 +125,17 @@ const groups = [
/* Each group carries its own tone so the eye can tell them apart mid-scroll: a colored icon
/* Each group carries its own tone so the eye can tell them apart mid-scroll: a colored icon
square, a matching left rule and a faint tinted background. */
square, a matching left rule and a faint tinted background. */
.wk-group { --g: var(--accent); --g-rgb: var(--accent-rgb); position: relative; padding: clamp(16px, 2.4vw, 24px); border-radius: var(--radius-lg); background: linear-gradient(135deg, rgba(var(--g-rgb), .07), transparent 55%), var(--surface); border: 1px solid var(--border); scroll-margin-top: 90px; }
.wk-group { --g: var(--accent); --g-rgb: var(--accent-rgb); position: relative; padding: clamp(16px, 2.4vw, 24px); border-radius: var(--radius-lg); background: linear-gradient(135deg, rgba(var(--g-rgb), .07), transparent 55%), var(--surface); border: 1px solid var(--border); scroll-margin-top: 90px; }
- .wk-group::before { content: ""; position: absolute; left: -1px; top: 18%; bottom: 18%; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }
+ /* The group's accent rule sits on its left edge beside the title only (not the length of the group). */
+ .wk-group__ico { position: relative; }
+ .wk-group__ico::before { content: ""; position: absolute; left: calc(-1 * clamp(16px, 2.4vw, 24px) - 2px); top: -2px; bottom: -2px; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }
.wk-group--b { --g: var(--accent-2); --g-rgb: var(--accent-2-rgb); }
.wk-group--b { --g: var(--accent-2); --g-rgb: var(--accent-2-rgb); }
.wk-group--c { --g: var(--accent-3); --g-rgb: var(--accent-3-rgb); }
.wk-group--c { --g: var(--accent-3); --g-rgb: var(--accent-3-rgb); }
.wk-group--d { --g: var(--violet); --g-rgb: 139, 92, 246; }
.wk-group--d { --g: var(--violet); --g-rgb: 139, 92, 246; }
.wk-group__head { display: grid; grid-template-columns: auto 1fr auto; gap: 14px 16px; align-items: center; margin-bottom: 16px; }
.wk-group__head { display: grid; grid-template-columns: auto 1fr auto; gap: 14px 16px; align-items: center; margin-bottom: 16px; }
.wk-group__ico { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); font-size: 1.05rem; box-shadow: 0 10px 24px -14px rgba(var(--g-rgb), .9); }
.wk-group__ico { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); font-size: 1.05rem; box-shadow: 0 10px 24px -14px rgba(var(--g-rgb), .9); }
.wk-group__txt { min-width: 0; }
.wk-group__txt { min-width: 0; }
- .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); display: flex; align-items: center; gap: 10px; }
+ .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); }
+ .wk-group__title .wk-group__n { display: inline-block; margin-left: 8px; vertical-align: .15em; line-height: 1.4; }
.wk-group__n { font-size: .72rem; padding: 2px 8px; border-radius: 999px; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); }
.wk-group__n { font-size: .72rem; padding: 2px 8px; border-radius: 999px; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); }
.wk-group__blurb { margin: 3px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.5; }
.wk-group__blurb { margin: 3px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.5; }
.wk-group__all { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(var(--g-rgb), .35); background: rgba(var(--g-rgb), .08); color: var(--fg); font: 600 .8rem var(--font-body); text-decoration: none; white-space: nowrap; transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease); }
.wk-group__all { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(var(--g-rgb), .35); background: rgba(var(--g-rgb), .08); color: var(--fg); font: 600 .8rem var(--font-body); text-decoration: none; white-space: nowrap; transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease); }
@@ -141,7 +144,7 @@ const groups = [
@@ -141,7 +144,7 @@ const groups = [
.wk-group__all:hover { text-decoration: none; background: rgba(var(--g-rgb), .16); border-color: rgba(var(--g-rgb), .6); transform: translateY(-1px); }
.wk-group__all:hover { text-decoration: none; background: rgba(var(--g-rgb), .16); border-color: rgba(var(--g-rgb), .6); transform: translateY(-1px); }
.wk-group__all:hover .wk-group__arrow { transform: translateX(3px); }
.wk-group__all:hover .wk-group__arrow { transform: translateX(3px); }
- .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
+ .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.wk-card { min-width: 0; }
.wk-card { min-width: 0; }
.wk-card__link { display: flex; flex-direction: column; height: 100%; border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--border); color: var(--fg); text-decoration: none; transition: transform .4s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease); }
.wk-card__link { display: flex; flex-direction: column; height: 100%; border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--border); color: var(--fg); text-decoration: none; transition: transform .4s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease); }
.wk-card__link:hover { text-decoration: none; transform: translateY(-3px); border-color: rgba(var(--g-rgb), .5); background: var(--surface-2); box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--g-rgb), .12), 0 16px 40px -22px rgba(var(--g-rgb), .7); }
.wk-card__link:hover { text-decoration: none; transform: translateY(-3px); border-color: rgba(var(--g-rgb), .5); background: var(--surface-2); box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--g-rgb), .12), 0 16px 40px -22px rgba(var(--g-rgb), .7); }
@@ -188,7 +191,6 @@ const groups = [
@@ -188,7 +191,6 @@ const groups = [
.work__all-go { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08); transition: transform .4s var(--ease), background .3s; }
.work__all-go { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08); transition: transform .4s var(--ease), background .3s; }
.work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }
.work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }
- @media (max-width: 1100px) { .wk-grid { grid-template-columns: repeat(3, 1fr); } }
- @media (max-width: 820px) { .wk-grid { grid-template-columns: repeat(2, 1fr); } .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }
+ @media (max-width: 820px) { .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }
@media (max-width: 480px) { .wk-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .wk-card__body { padding: 10px 11px; } .wk-card__text { display: none; } .wk-card__title { font-size: .9rem; } .wk-card--img .wk-card__meta { display: none; } .work__all-btn { padding: 18px; gap: 14px; } .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; } }
@media (max-width: 480px) { .wk-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .wk-card__body { padding: 10px 11px; } .wk-card__text { display: none; } .wk-card__title { font-size: .9rem; } .wk-card--img .wk-card__meta { display: none; } .work__all-btn { padding: 18px; gap: 14px; } .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; } }
</style>
</style>
@@ -153,7 +153,7 @@ const imageUrl = new URL(image, site).toString();
@@ -153,7 +153,7 @@ const imageUrl = new URL(image, site).toString();
// Text on top of the accent: relative luminance decides dark ink vs white.
// Text on top of the accent: relative luminance decides dark ink vs white.
var rgb = hexToRgbStr(accent).split(', ').map(function (v) { v = parseInt(v, 10) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });
var rgb = hexToRgbStr(accent).split(', ').map(function (v) { v = parseInt(v, 10) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });
var lum = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
var lum = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
- root.setProperty('--on-accent', lum > 0.36 ? '#0b0f1a' : '#ffffff');
+ root.setProperty('--on-accent', lum > 0.5 ? '#0b0f1a' : '#ffffff');
root.setProperty('--accent', accent);
root.setProperty('--accent', accent);
root.setProperty('--accent-rgb', hexToRgbStr(accent));
root.setProperty('--accent-rgb', hexToRgbStr(accent));
root.setProperty('--violet', violet);
root.setProperty('--violet', violet);
@@ -85,8 +85,8 @@ const catLabel: Record<string, string> = {
@@ -85,8 +85,8 @@ const catLabel: Record<string, string> = {
.pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }
.pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }
.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }
.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }
.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }
.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }
- .pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }
- .pp__meta-wide { grid-column: 1 / -1; }
+ .pp__meta { display: flex; flex-wrap: wrap; gap: 14px 36px; margin: 1.4rem 0; }
+ .pp__meta-wide { flex-basis: 100%; }
.pp__meta strong { display: block; color: var(--accent); font-family: var(--font-mono); font-size: .72rem; text-transform: uppercase; letter-spacing: .14em; margin-bottom: 6px; }
.pp__meta strong { display: block; color: var(--accent); font-family: var(--font-mono); font-size: .72rem; text-transform: uppercase; letter-spacing: .14em; margin-bottom: 6px; }
.pp__meta span { color: var(--muted); font-size: .94rem; }
.pp__meta span { color: var(--muted); font-size: .94rem; }
.pp__tech { display: flex; flex-wrap: wrap; gap: 6px; }
.pp__tech { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -105,5 +105,6 @@ const catLabel: Record<string, string> = {
@@ -105,5 +105,6 @@ const catLabel: Record<string, string> = {
.pp__nav-link .mono { font-size: .72rem; color: var(--dim); letter-spacing: .1em; text-transform: uppercase; }
.pp__nav-link .mono { font-size: .72rem; color: var(--dim); letter-spacing: .1em; text-transform: uppercase; }
.pp__nav-link:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateY(-3px); text-decoration: none; }
.pp__nav-link:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateY(-3px); text-decoration: none; }
.pp__nav-link--next { text-align: right; }
.pp__nav-link--next { text-align: right; }
+ @media (max-width: 560px) { .pp { padding-bottom: 48px; } .pp__prose { padding: 18px; } }
@media (max-width: 820px) { .pp__body { grid-template-columns: 1fr; } .pp__side { position: static; } .pp__nav { grid-template-columns: 1fr; } .pp__nav-link--next { text-align: left; } }
@media (max-width: 820px) { .pp__body { grid-template-columns: 1fr; } .pp__side { position: static; } .pp__nav { grid-template-columns: 1fr; } .pp__nav-link--next { text-align: left; } }
</style>
</style>
@@ -116,6 +116,7 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);
@@ -116,6 +116,7 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);
.resume__contact a:hover { color: var(--accent); text-decoration: none; }
.resume__contact a:hover { color: var(--accent); text-decoration: none; }
.resume__contact i { color: var(--accent); width: 14px; }
.resume__contact i { color: var(--accent); width: 14px; }
.resume__actions { display: flex; gap: 10px; flex-wrap: wrap; padding: 22px 0 6px; }
.resume__actions { display: flex; gap: 10px; flex-wrap: wrap; padding: 22px 0 6px; }
+ @media (max-width: 560px) { .resume__actions { display: grid; grid-template-columns: 1fr; } .resume__actions > .btn { justify-content: center; } }
.resume__pdf-note { font-size: .82rem; color: var(--dim); max-width: 62ch; margin: 0 0 8px; }
.resume__pdf-note { font-size: .82rem; color: var(--dim); max-width: 62ch; margin: 0 0 8px; }
.resume__pdf-note strong { color: var(--muted); }
.resume__pdf-note strong { color: var(--muted); }
.themed-pdf-caret { font-size: .68em; transition: transform .3s var(--ease); }
.themed-pdf-caret { font-size: .68em; transition: transform .3s var(--ease); }
@@ -55,6 +55,22 @@ body::after {
@@ -55,6 +55,22 @@ body::after {
animation: grain 1.4s steps(4) infinite;
animation: grain 1.4s steps(4) infinite;
}
}
:root[data-theme="light"] body::after { opacity: .035; }
:root[data-theme="light"] body::after { opacity: .035; }
+/* Phones: the grid was so faint (and masked so tight) that only the jittering grain showed, which
+ read as the screen glitching. Here the grid is clearly there over more of the screen, and the
+ grain is a slower, lighter texture. */
+@media (max-width: 900px) {
+ body::before {
+ background-image:
+ linear-gradient(to right, rgba(160,180,255,.085) 1px, transparent 1px),
+ linear-gradient(to bottom, rgba(160,180,255,.085) 1px, transparent 1px);
+ background-size: 48px 48px;
+ mask-image: linear-gradient(to bottom, transparent 0, transparent 96px, #000 170px),
+ radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);
+ -webkit-mask-image: -webkit-linear-gradient(top, transparent 0, transparent 96px, #000 170px),
+ -webkit-radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);
+ }
+ body::after { opacity: .035; animation-duration: 4s; }
+}
@keyframes grain {
@keyframes grain {
0% { transform: translate(0, 0); } 25% { transform: translate(-3%, 2%); }
0% { transform: translate(0, 0); } 25% { transform: translate(-3%, 2%); }
50% { transform: translate(2%, -3%); } 75% { transform: translate(-1%, -1%); } 100% { transform: translate(0, 0); }
50% { transform: translate(2%, -3%); } 75% { transform: translate(-1%, -1%); } 100% { transform: translate(0, 0); }
@@ -95,7 +111,7 @@ p { line-height: 1.7; color: var(--muted); margin: 0 0 1em; }
@@ -95,7 +111,7 @@ p { line-height: 1.7; color: var(--muted); margin: 0 0 1em; }
strong { color: var(--fg); font-weight: 600; }
strong { color: var(--fg); font-weight: 600; }
.container { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }
.container { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }
-.section { padding: clamp(52px, 7.5vw, 116px) 0; position: relative; }
+.section { padding: clamp(36px, 6vw, 88px) 0; position: relative; }
/* A hairline between sections plus a faint alternating band so sections still read as distinct
/* A hairline between sections plus a faint alternating band so sections still read as distinct
blocks when someone is flicking through the page fast. */
blocks when someone is flicking through the page fast. */
.section + .section::before, main > .section:not(.hero):first-child::before {
.section + .section::before, main > .section:not(.hero):first-child::before {
@@ -142,6 +158,9 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
@@ -142,6 +158,9 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
transition: transform .5s var(--ease), border-color .4s var(--ease), box-shadow .5s var(--ease), background .4s var(--ease);
transition: transform .5s var(--ease), border-color .4s var(--ease), box-shadow .5s var(--ease), background .4s var(--ease);
}
}
.card:hover { border-color: rgba(var(--accent-rgb), .35); box-shadow: var(--shadow-card), var(--shadow-glow); }
.card:hover { border-color: rgba(var(--accent-rgb), .35); box-shadow: var(--shadow-card), var(--shadow-glow); }
+/* Phones: no backdrop blur on cards (little to see through on a flat background, and every blurred
+ card is another layer the phone must composite; dropped layers painted as blank boxes). */
+@media (max-width: 900px) { .card { -webkit-backdrop-filter: none; backdrop-filter: none; } }
/* Pointer-tracked spotlight (set --mx/--my in JS) */
/* Pointer-tracked spotlight (set --mx/--my in JS) */
.spot { --mx: 50%; --my: 50%; }
.spot { --mx: 50%; --my: 50%; }
@@ -163,7 +182,6 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
@@ -163,7 +182,6 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
font: 600 .98rem/1 var(--font-body); letter-spacing: .005em;
font: 600 .98rem/1 var(--font-body); letter-spacing: .005em;
cursor: pointer; text-decoration: none; white-space: nowrap;
cursor: pointer; text-decoration: none; white-space: nowrap;
transition: transform .35s var(--ease), border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease), box-shadow .35s var(--ease);
transition: transform .35s var(--ease), border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease), box-shadow .35s var(--ease);
- will-change: transform;
}
}
.btn:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .6); background: rgba(var(--accent-rgb), .1); transform: translateY(-1px); }
.btn:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .6); background: rgba(var(--accent-rgb), .1); transform: translateY(-1px); }
.btn i { font-size: .95em; }
.btn i { font-size: .95em; }
@@ -172,6 +190,7 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
@@ -172,6 +190,7 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
highlight and inner ring keep it from reading as one flat slab of color. */
highlight and inner ring keep it from reading as one flat slab of color. */
.btn--primary {
.btn--primary {
--btn-bg: var(--accent); color: var(--on-accent); border-color: transparent; overflow: hidden;
--btn-bg: var(--accent); color: var(--on-accent); border-color: transparent; overflow: hidden;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, .18);
background-image: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 60%);
background-image: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 60%);
box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 0 0 1px rgba(0,0,0,.08), 0 12px 30px -12px rgba(var(--accent-rgb), .75);
box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 0 0 1px rgba(0,0,0,.08), 0 12px 30px -12px rgba(var(--accent-rgb), .75);
}
}
@@ -22,7 +22,7 @@
@@ -22,7 +22,7 @@
--accent-3: #3ee6b0;
--accent-3: #3ee6b0;
--accent-3-rgb: 62, 230, 176;
--accent-3-rgb: 62, 230, 176;
--violet: #8b5cf6;
--violet: #8b5cf6;
- --on-accent: #06091a;
+ --on-accent: #ffffff;
--grad: linear-gradient(95deg, var(--accent) 0%, var(--violet) 48%, var(--accent-2) 100%);
--grad: linear-gradient(95deg, var(--accent) 0%, var(--violet) 48%, var(--accent-2) 100%);
--grad-soft: linear-gradient(135deg, rgba(var(--accent-rgb), .22), rgba(139, 92, 246, .18) 50%, rgba(var(--accent-2-rgb), .16));
--grad-soft: linear-gradient(135deg, rgba(var(--accent-rgb), .22), rgba(139, 92, 246, .18) 50%, rgba(var(--accent-2-rgb), .16));