diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..4dfde31 --- /dev/null +++ b/.air.toml @@ -0,0 +1,97 @@ +#:schema https://json.schemastore.org/any.json + +env_files = [] +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + + # Your Go entrypoint lives here + cmd = "go build -o ./tmp/main ./cmd/server" + + # Faster feedback for UI work + delay = 200 + + entrypoint = ["./tmp/main"] + + exclude_dir = [ + ".git", + "tmp", + "vendor", + "testdata", + "node_modules" + ] + + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = true + follow_symlink = false + full_bin = "" + + include_dir = [] + + # Watch your actual frontend files too + include_ext = [ + "go", + "tpl", + "tmpl", + "html", + "css", + "js", + "svg" + ] + + include_file = [] + + kill_delay = "500ms" + log = "build-errors.log" + + poll = false + poll_interval = 0 + + post_cmd = [] + pre_cmd = [] + + rerun = false + rerun_delay = 500 + + send_interrupt = true + + # Don't silently continue using an old binary if the new build fails + stop_on_error = true + +[color] + app = "" + build = "yellow" + main = "magenta" + mode = "" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = true + +# Air has built-in browser live reload +[proxy] + enabled = true + + # Your Go server + app_port = 8080 + + # Open THIS in Chrome + proxy_port = 8090 + + # Give your app a moment to restart + app_start_timeout = 10000 + +[screen] + clear_on_rebuild = false + keep_scroll = true \ No newline at end of file diff --git a/internal/http/home_test.go b/internal/http/home_test.go index 7fe4913..2dc748a 100644 --- a/internal/http/home_test.go +++ b/internal/http/home_test.go @@ -245,21 +245,37 @@ func TestHomeIncludesSelectedWorkAndTooling(t *testing.T) { t.Fatalf("status = %d", rec.Code) } body := rec.Body.String() + // Verify the current editorial content, not IDs and secondary repository + // links belonging to the retired full-length tooling section. for _, expected := range []string{ `href="/work/salon-rebuild/"`, `href="/work/portfolio"`, - `id="workspace-project-title"`, `id="tooling-title"`, + `id="selected-work-title"`, `id="capabilities-title"`, + `Portfolio & Client Workspace`, + `go-jsonld-schema`, `djm-cli`, `https://github.com/danieljmanningdev/djm-cli`, `https://github.com/danieljmanningdev/go-jsonld-schema`, - `https://github.com/danieljmanningdev/go-web-core`, - `https://github.com/danieljmanningdev/go-web-security`, - `https://github.com/danieljmanningdev/go-web-auth`, - `https://github.com/danieljmanningdev/go-starter-auth-app`, + `data-theme="light"`, + `class="editorial-feature__media"`, + `salon-rebuild-home-480.avif`, `salon-rebuild-home-480.webp`, + `site-ending--home`, + `aria-labelledby="contact-title"`, + `href="mailto:daniel@danieljmanningdev.com"`, } { if !strings.Contains(body, expected) { t.Errorf("missing %q", expected) } } - if strings.Contains(body, `marquee-track`) { - t.Error("homepage must not autoplay an unpausable capability list") + for _, removed := range []string{ + `marquee-track`, + `daniel-avatar.svg`, + `class="footer-cta"`, + `Make the complex feel`, + } { + if strings.Contains(body, removed) { + t.Errorf("homepage must not restore removed content %q", removed) + } + } + if count := strings.Count(body, `id="contact"`); count != 1 { + t.Errorf("expected one contact section, got %d", count) } } diff --git a/scripts/check-browser.cjs b/scripts/check-browser.cjs index 1793406..dba6096 100644 --- a/scripts/check-browser.cjs +++ b/scripts/check-browser.cjs @@ -48,6 +48,19 @@ async function check(name, callback) { catch (error) { failures.push({ name, error: String(error.stack || error) }); results.push({ name, passed: false }); } } +async function decodeVisibleImage(page, image) { + await image.scrollIntoViewIfNeeded(); + const element = await image.elementHandle(); + try { + // Scrolling schedules native lazy loading; Firefox may not have a valid + // image request yet. Wait for loaded pixels instead of racing decode(). + await page.waitForFunction(el => el.complete && el.naturalWidth > 0, element); + await image.evaluate(el => el.decode()); + } finally { + await element.dispose(); + } +} + function overflowInPage() { const width = document.documentElement.clientWidth; const bad = []; @@ -84,6 +97,14 @@ function overflowInPage() { const response = await page.goto(base + route, { waitUntil: 'load' }); assert.equal(response.status(), status, 'Unexpected document status'); await page.evaluate(() => document.fonts.ready); + if (route === '/') { + // A full-page screenshot alone does not trigger every lazy image. + // Scroll and decode all homepage assets, then restore the top. + for (const image of await page.locator('img').all()) { + await decodeVisibleImage(page, image); + } + await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); + } await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); const overflow = await page.evaluate(overflowInPage); assert.ok(overflow.scrollWidth <= width + 1, JSON.stringify(overflow)); @@ -99,10 +120,11 @@ function overflowInPage() { const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']).analyze(); const name = `${engine}-${route.replace(/[^a-z0-9]/gi, '_') || 'home'}-${width}`; fs.writeFileSync(path.join(reportDirectory, `${name}.axe.json`), JSON.stringify({ url: route, violations: scan.violations, incomplete: scan.incomplete }, null, 2)); - assert.deepEqual(scan.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => n.target) })), [], 'Automated accessibility findings'); + // Keep screenshots even when an accessibility assertion fails. if (route === '/' || (route === '/work/portfolio' && width === 390)) { await page.screenshot({ path: path.join(reportDirectory, `${name}.png`), fullPage: true }); } + assert.deepEqual(scan.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => n.target) })), [], 'Automated accessibility findings'); } }); } @@ -127,28 +149,57 @@ function overflowInPage() { await page.keyboard.press('Enter'); assert.equal(await menu.getAttribute('open'), null); }); - await check(`${engine} images decode and use responsive source`, async () => { + await check(`${engine} images decode and use responsive sources`, async () => { await page.goto(base + '/'); - const image = page.locator('.home-work-preview__image'); - await image.scrollIntoViewIfNeeded(); - await page.waitForFunction(() => { - const el = document.querySelector('.home-work-preview__image'); - return el && el.complete && el.naturalWidth > 0; - }); - await image.evaluate(el => el.decode()); - const info = await image.evaluate(el => ({ source: el.currentSrc, width: el.naturalWidth, height: el.naturalHeight })); - assert.ok(info.width > 0 && info.height > 0); - assert.match(info.source, /salon-rebuild-home-(480|960|1600)\.(avif|webp)$/); - results.push({ name: `${engine} responsive image selected`, ...info, passed: true }); + for (const selector of ['.editorial-feature__media img', '.editorial-project--lead .editorial-project__media img']) { + const image = page.locator(selector); + await decodeVisibleImage(page, image); + const info = await image.evaluate(el => ({ source: el.currentSrc, width: el.naturalWidth, height: el.naturalHeight })); + assert.ok(info.width > 0 && info.height > 0); + assert.match(info.source, /salon-rebuild-home-(480|960|1600)\.(avif|webp)$/); + results.push({ name: `${engine} responsive image selected: ${selector}`, ...info, passed: true }); + } }); + for (const width of widths) { + await check(`${engine} dark homepage ending ${width}px`, async () => { + await page.setViewportSize({ width, height: 900 }); + await page.goto(base + '/', { waitUntil: 'load' }); + assert.equal(await page.locator('main[data-theme="light"]').count(), 1); + assert.equal(await page.locator('.editorial-hero__avatar').count(), 0, 'Removed avatar must not return'); + assert.equal(await page.locator('.footer-cta').count(), 0, 'Removed duplicate CTA must not return'); + assert.equal(await page.locator('#contact').count(), 1); + await page.evaluate(() => window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' })); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + const ending = await page.evaluate(() => { + const header = document.querySelector('.site-header'); + const panel = document.querySelector('.site-ending--home'); + const footer = document.querySelector('.site-footer'); + return { + headerBottom: header.getBoundingClientRect().bottom, + panelTop: panel.getBoundingClientRect().top, + footerTop: footer.getBoundingClientRect().top, + footerBottom: footer.getBoundingClientRect().bottom, + viewport: window.innerHeight, + colours: [header, panel, document.querySelector('#contact'), footer].map(el => getComputedStyle(el).backgroundColor), + }; + }); + assert.ok(ending.panelTop <= ending.headerBottom + 1, 'Light content must not show between header and ending'); + assert.ok(Math.abs(ending.footerBottom - ending.viewport) <= 1, 'Footer must reach the viewport bottom'); + assert.ok(ending.footerTop > ending.viewport / 2, 'Footer belongs at the end, below the contact content'); + assert.equal(new Set(ending.colours).size, 1, 'Header, contact and footer must share the shell colour token'); + if (width === 390 || width === 1440) { + await page.screenshot({ path: path.join(reportDirectory, `${engine}-home-ending-${width}.png`) }); + } + }); + } await check(`${engine} no JavaScript content and navigation`, async () => { const noJS = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 900 }, reducedMotion: 'reduce' }); try { const noJSPage = await noJS.newPage(); await noJSPage.goto(base + '/', { waitUntil: 'domcontentloaded' }); // DOM readiness can precede stylesheet layout, especially in Firefox. - await noJSPage.locator('#workspace-project-title').waitFor({ state: 'visible' }); - await noJSPage.locator('#tooling-title').waitFor({ state: 'visible' }); + await noJSPage.getByRole('heading', { name: 'Portfolio & Client Workspace', exact: true }).waitFor({ state: 'visible' }); + await noJSPage.getByRole('heading', { name: 'go-jsonld-schema', exact: true }).waitFor({ state: 'visible' }); await noJSPage.locator('.mobile-nav summary').click(); await noJSPage.locator('.mobile-nav-link').first().waitFor({ state: 'visible' }); } finally { diff --git a/web/static/css/vanilla/pages/home.css b/web/static/css/vanilla/pages/home.css index fb88b3a..e734481 100644 --- a/web/static/css/vanilla/pages/home.css +++ b/web/static/css/vanilla/pages/home.css @@ -1,252 +1,445 @@ /* ========================================================================== - HOME PAGE + HOME PAGE — MINIMAL EDITORIAL REDESIGN Daniel J. Manning https://danieljmanningdev.com ========================================================================== */ -.home-page { overflow: clip; } - -/* Hero */ -.home-hero { position: relative; isolation: isolate; border-bottom: 1px solid color-mix(in oklab, white 8%, transparent); } -.home-hero__glow-field { position: absolute; inset: 0; z-index: -10; overflow: hidden; pointer-events: none; } -.home-hero__glow { position: absolute; width: 20rem; height: 20rem; border-radius: 999px; filter: blur(64px); } -.home-hero__glow--signal { top: 5rem; left: 5%; background: color-mix(in oklab, var(--color-signal-500) 13%, transparent); } -.home-hero__glow--electric { top: 33.333%; right: -6rem; background: color-mix(in oklab, var(--color-electric-400) 9%, transparent); } -.home-hero__layout { display: grid; min-height: calc(100svh - 5rem); align-items: center; gap: 3.5rem; padding-block: 5.5rem; } -.home-hero__copy { max-width: 64rem; } -.home-hero__identity { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; } -.home-hero__identity-line { width: 2.5rem; height: 1px; background: var(--color-signal-400); } -.home-hero__identity-text { color: color-mix(in oklab, var(--color-signal-300) 70%, transparent); font-size: 0.75rem; letter-spacing: 0.3em; text-transform: uppercase; } -.home-hero__title { font-size: clamp(3.125rem, 12vw, 4.25rem); font-weight: 500; line-height: 0.92; letter-spacing: -0.045em; } -.home-hero__title-line { display: block; } -.home-hero__title-accent { color: var(--color-signal-400); } -.home-hero__intro { max-width: 42rem; margin-top: 2.25rem; color: var(--color-mist-300); font-size: 1.125rem; line-height: 2rem; } -.home-hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2rem; } -.home-hero__metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 48rem; gap: 1.5rem; margin-top: 3rem; padding-top: 1.75rem; border-top: 1px solid color-mix(in oklab, white 9%, transparent); } -.home-hero__metric--wide { grid-column: span 2; } -.home-hero__metric-value { margin-top: 0.5rem; color: var(--color-mist-100); font-size: 0.875rem; font-weight: 600; } - -/* Hero workspace preview */ -.home-workspace { position: relative; width: 100%; max-width: 36rem; margin-inline: auto; } -.home-workspace__glow { position: absolute; inset: -2rem; z-index: -10; border-radius: 3rem; background: linear-gradient(135deg, color-mix(in oklab, var(--color-signal-500) 16%, transparent), transparent, color-mix(in oklab, var(--color-electric-400) 10%, transparent)); filter: blur(64px); } -.home-workspace__panel { overflow: hidden; border-radius: 2.75rem; } -.home-workspace__toolbar { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; border-bottom: 1px solid color-mix(in oklab, white 8%, transparent); } -.home-workspace__dots { display: flex; gap: 0.5rem; } -.home-workspace__dot { width: 0.5rem; height: 0.5rem; border-radius: 999px; background: color-mix(in oklab, white 18%, transparent); } -.home-workspace__dot--accent { background: color-mix(in oklab, var(--color-signal-300) 70%, transparent); } -.home-workspace__label { color: var(--color-mist-700); font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.18em; text-transform: uppercase; } -.home-workspace__body { padding: 1.25rem; } -.home-workspace__heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.25rem; } -.home-workspace__principle { margin-top: 0.5rem; color: var(--color-mist-50); font-size: 1.5rem; font-weight: 500; letter-spacing: -0.035em; } -.home-workspace__status { display: inline-flex; flex-shrink: 0; align-items: center; gap: 0.5rem; padding: 0.375rem 0.75rem; border: 1px solid color-mix(in oklab, var(--color-signal-300) 20%, transparent); border-radius: 999px; background: color-mix(in oklab, var(--color-signal-300) 7%, transparent); color: var(--color-signal-200); font-size: 0.75rem; font-weight: 700; } -.home-workspace__status-dot { width: 0.375rem; height: 0.375rem; border-radius: 999px; background: var(--color-signal-300); } -.home-workspace__cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; margin-top: 1.75rem; } -.home-workspace__card { padding: 1.25rem; border: 1px solid color-mix(in oklab, white 8%, transparent); border-radius: 1.5rem; background: color-mix(in oklab, black 18%, transparent); } -.home-workspace__card-title { margin-top: 1.25rem; color: var(--color-mist-50); font-size: 1.125rem; font-weight: 600; } -.home-workspace__card-copy { margin-top: 0.5rem; color: var(--color-mist-500); font-size: 0.75rem; line-height: 1.25rem; } -.home-workspace__flow { grid-column: span 2; padding: 1.25rem; border: 1px solid color-mix(in oklab, white 8%, transparent); border-radius: 1.5rem; background: #050915; } -.home-workspace__flow-header { display: flex; align-items: center; justify-content: space-between; } -.home-workspace__live { color: var(--color-signal-300); font-family: var(--font-mono); font-size: 0.65rem; } -.home-workspace__steps { display: grid; grid-template-columns: auto 1fr; gap: 1rem; margin-top: 1.25rem; font-size: 0.875rem; } -.home-workspace__step-number { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: 0.75rem; background: color-mix(in oklab, var(--color-signal-400) 12%, transparent); color: var(--color-signal-200); font-family: var(--font-mono); font-size: 0.75rem; } -.home-workspace__step-title { color: var(--color-mist-100); font-weight: 600; } -.home-workspace__step-copy { margin-top: 0.25rem; color: var(--color-mist-700); font-size: 0.75rem; } -.home-capability-strip { padding-block: 1rem; border-top: 1px solid color-mix(in oklab, white 7%, transparent); } -.home-capability-strip__track { display: flex; width: max-content; align-items: center; gap: 2.5rem; padding-right: 2.5rem; color: var(--color-mist-700); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.24em; text-transform: uppercase; white-space: nowrap; } -.home-capability-strip__accent { color: var(--color-signal-300); } - -/* Shared homepage section structure */ -.home-section { scroll-margin-top: 6rem; border-bottom: 1px solid color-mix(in oklab, white 8%, transparent); padding-block: 6rem; } -.home-section__intro { display: grid; gap: 2rem; } -.home-section__title { margin-top: 1.75rem; color: var(--color-mist-50); font-size: 2.25rem; font-weight: 500; line-height: 1.02; letter-spacing: -0.045em; text-wrap: balance; } -.home-section__copy { max-width: 42rem; color: var(--color-mist-500); font-size: 1.125rem; line-height: 2rem; } -.home-gradient-text { background: linear-gradient(90deg, var(--color-signal-300), var(--color-electric-300)); background-clip: text; -webkit-background-clip: text; color: transparent; } -.home-accent-text { color: var(--color-signal-300); } - -/* Selected work */ -.home-work-intro { align-items: end; } -.home-work-card { display: block; margin-top: 3.5rem; padding: 1rem; border-radius: 2.75rem; } -.home-work-card__layout { display: grid; gap: 2.5rem; } -.home-work-card__copy { position: relative; z-index: 10; padding-block: 1.25rem; } -.home-work-card__meta { display: flex; align-items: center; gap: 0.75rem; } -.home-work-card__badge { padding: 0.375rem 0.75rem; border: 1px solid color-mix(in oklab, var(--color-signal-300) 20%, transparent); border-radius: 999px; background: color-mix(in oklab, var(--color-signal-300) 7%, transparent); color: var(--color-signal-200); font-size: 0.67rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; } -.home-work-card__year { color: var(--color-mist-700); font-size: 0.75rem; } -.home-work-card__title { margin-top: 2rem; color: var(--color-mist-50); font-size: 2.25rem; font-weight: 500; line-height: 0.98; letter-spacing: -0.045em; text-wrap: balance; } -.home-work-card__title-accent { display: block; } -.home-work-card__description { max-width: 36rem; margin-top: 1.5rem; color: var(--color-mist-500); line-height: 1.75rem; } -.home-work-card__tags { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 2rem; color: var(--color-mist-300); font-size: 0.75rem; } -.home-work-card__tag { padding: 0.375rem 0.75rem; border: 1px solid color-mix(in oklab, white 9%, transparent); border-radius: 999px; } -.home-work-card__link { display: inline-flex; align-items: center; gap: 0.75rem; margin-top: 2.5rem; color: var(--color-mist-100); font-size: 0.875rem; font-weight: 700; } -.home-work-card__arrow { color: var(--color-signal-300); transition: transform var(--duration-fast) var(--ease-standard); } -.home-work-card:hover .home-work-card__arrow { transform: translateX(0.25rem); } -.home-work-preview { position: relative; min-height: 27rem; overflow: hidden; padding: 1.25rem; border: 1px solid color-mix(in oklab, white 9%, transparent); border-radius: 2rem; background: linear-gradient(135deg, color-mix(in oklab, var(--color-signal-500) 12%, transparent), #071127, color-mix(in oklab, var(--color-electric-400) 7%, transparent)); } -.home-work-preview__glow { position: absolute; top: -4rem; right: -4rem; width: 16rem; height: 16rem; border-radius: 999px; background: color-mix(in oklab, var(--color-signal-400) 14%, transparent); filter: blur(64px); } -.home-work-preview__window { position: relative; display: grid; height: 100%; grid-template-rows: auto 1fr; overflow: hidden; border: 1px solid color-mix(in oklab, white 10%, transparent); border-radius: 1.5rem; background: rgb(5 11 24 / 0.92); box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.4); } -.home-work-preview__toolbar { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; border-bottom: 1px solid color-mix(in oklab, white 8%, transparent); } -.home-work-preview__dots { display: flex; align-items: center; gap: 0.5rem; } -.home-work-preview__dot { width: 0.5rem; height: 0.5rem; border-radius: 999px; background: color-mix(in oklab, white 18%, transparent); } -.home-work-preview__dot--accent { background: color-mix(in oklab, var(--color-signal-300) 70%, transparent); } -.home-work-preview__label { color: var(--color-mist-700); font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.15em; text-transform: uppercase; } -.home-work-preview__body { display: grid; gap: 1rem; padding: 1.25rem; } -.home-work-preview__sidebar, .home-work-preview__surface, .home-work-preview__stat { padding: 1rem; border: 1px solid color-mix(in oklab, white 8%, transparent); border-radius: 1rem; background: color-mix(in oklab, white 3%, transparent); } -.home-work-preview__accent-line { width: 4rem; height: 0.5rem; border-radius: 999px; background: color-mix(in oklab, var(--color-signal-300) 35%, transparent); } -.home-work-preview__nav { display: grid; gap: 0.75rem; margin-top: 1.75rem; } -.home-work-preview__nav-line { height: 2rem; border-radius: 0.75rem; background: color-mix(in oklab, white 5%, transparent); } -.home-work-preview__nav-line--active { background: color-mix(in oklab, var(--color-signal-400) 10%, transparent); } -.home-work-preview__content { display: grid; align-content: start; gap: 1rem; } -.home-work-preview__stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem; } -.home-work-preview__stat-line { width: 2.5rem; height: 0.5rem; border-radius: 999px; background: color-mix(in oklab, white 12%, transparent); } -.home-work-preview__stat-value { width: 3rem; height: 1.75rem; margin-top: 1.25rem; border-radius: 0.5rem; background: color-mix(in oklab, var(--color-signal-300) 25%, transparent); } -.home-work-preview__stat-value--electric { background: color-mix(in oklab, var(--color-electric-300) 20%, transparent); } -.home-work-preview__stat-value--indigo { background: rgb(165 180 252 / 0.2); } -.home-work-preview__surface-header { display: flex; align-items: center; justify-content: space-between; } -.home-work-preview__surface-title { width: 6rem; height: 0.5rem; border-radius: 999px; background: color-mix(in oklab, white 12%, transparent); } -.home-work-preview__surface-action { width: 5rem; height: 1.75rem; border-radius: 999px; background: color-mix(in oklab, var(--color-signal-400) 12%, transparent); } -.home-work-preview__rows { display: grid; gap: 0.75rem; margin-top: 1.25rem; } -.home-work-preview__row { height: 3rem; border-radius: 0.75rem; background: color-mix(in oklab, white 5%, transparent); } - -/* Capabilities */ -.home-capabilities__layout { display: grid; gap: 3rem; } -.home-capabilities__intro-title { max-width: 36rem; } -.home-capabilities__intro-copy { max-width: 32rem; margin-top: 1.75rem; color: var(--color-mist-500); line-height: 1.75rem; } -.home-capabilities__list { border-block: 1px solid color-mix(in oklab, white 9%, transparent); } -.home-capability { display: grid; gap: 1.25rem; padding-block: 2.25rem; } -.home-capability + .home-capability { border-top: 1px solid color-mix(in oklab, white 9%, transparent); } -.home-capability__number { color: var(--color-signal-300); font-family: var(--font-mono); font-size: 0.875rem; } -.home-capability__title { color: var(--color-mist-50); font-size: 1.5rem; font-weight: 500; } -.home-capability__copy { max-width: 42rem; margin-top: 1rem; color: var(--color-mist-500); line-height: 1.75rem; } - -/* Working style */ -.home-style__layout { display: grid; gap: 3.5rem; } -.home-style__intro-copy { max-width: 36rem; margin-top: 1.75rem; color: var(--color-mist-500); font-size: 1.125rem; line-height: 2rem; } -.home-style__cards { display: grid; gap: 1rem; } -.home-style__card { padding: 1.5rem; border-radius: 2rem; } -.home-style__number { color: var(--color-signal-300); font-family: var(--font-mono); font-size: 0.875rem; } -.home-style__number--electric { color: var(--color-electric-300); } -.home-style__card-title { margin-top: 4rem; color: var(--color-mist-50); font-size: 1.25rem; font-weight: 600; } -.home-style__card-copy { margin-top: 0.75rem; color: var(--color-mist-500); font-size: 0.875rem; line-height: 1.5rem; } - -/* Journal */ -.home-journal-section { padding-block: 6rem; border-bottom: 1px solid color-mix(in oklab, white 8%, transparent); } -.home-journal-card { display: grid; gap: 2.5rem; padding: 2rem; border-radius: 2.75rem; } -.home-journal-card__title { max-width: 56rem; } -.home-journal-card__copy { max-width: 42rem; margin-top: 1.5rem; color: var(--color-mist-500); font-size: 1.125rem; line-height: 2rem; } -.home-journal-card__arrow { position: relative; z-index: 10; display: grid; width: 4rem; height: 4rem; place-items: center; border: 1px solid color-mix(in oklab, var(--color-signal-300) 25%, transparent); border-radius: 999px; background: color-mix(in oklab, var(--color-signal-300) 8%, transparent); color: var(--color-signal-200); font-size: 1.5rem; transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); } -.home-journal-card:hover .home-journal-card__arrow { background: var(--color-signal-400); color: white; } - -/* Contact */ -.home-contact { scroll-margin-top: 6rem; padding-block: 6rem; } -.home-contact__panel { position: relative; overflow: hidden; padding: 2rem; border: 1px solid color-mix(in oklab, white 9%, transparent); border-radius: 2.75rem; background: linear-gradient(135deg, color-mix(in oklab, white 6%, transparent), color-mix(in oklab, white 2%, transparent), color-mix(in oklab, var(--color-signal-400) 7%, transparent)); } -.home-contact__glow { position: absolute; top: -7rem; right: -7rem; width: 24rem; height: 24rem; border-radius: 999px; background: color-mix(in oklab, var(--color-signal-400) 12%, transparent); filter: blur(64px); pointer-events: none; } -.home-contact__layout { position: relative; display: grid; gap: 3rem; } -.home-contact__title { max-width: 64rem; margin-top: 1.75rem; color: var(--color-mist-50); font-size: clamp(3rem, 7vw, 7rem); font-weight: 500; line-height: 0.9; letter-spacing: -0.06em; text-wrap: balance; } -.home-contact__title-accent { display: block; } -.home-contact__aside { max-width: 24rem; } -.home-contact__copy { color: var(--color-mist-500); line-height: 1.75rem; } -.home-contact__button { margin-top: 1.75rem; } +.home-page { + overflow: clip; + background: var(--color-background); + color: var(--color-text-primary); +} -@media (min-width: 40rem) { - .home-hero__glow { width: 28rem; height: 28rem; } - .home-hero__title { font-size: 6rem; } - .home-hero__intro { font-size: 1.25rem; } - .home-hero__metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .home-hero__metric--wide { grid-column: span 1; } - .home-workspace__body { padding: 1.75rem; } - .home-section { padding-block: 7.5rem; } - .home-section__title { font-size: 3.75rem; } - .home-work-card { padding: 1.75rem; } - .home-work-card__copy { padding-inline: 0.75rem; } - .home-work-card__title { font-size: 3rem; } - .home-work-preview { min-height: 34rem; padding: 1.75rem; } - .home-work-preview__body { grid-template-columns: 0.38fr 1fr; } - .home-capability { grid-template-columns: 4rem 1fr; } - .home-capability__title { font-size: 1.875rem; } - .home-style__cards { grid-template-columns: repeat(3, minmax(0, 1fr)); } - .home-style__card { min-height: 18rem; } - .home-style__card:nth-child(2) { transform: translateY(2rem); } - .home-style__card:nth-child(3) { transform: translateY(4rem); } - .home-journal-section { padding-block: 7.5rem; } - .home-journal-card { padding: 3rem; } - .home-contact { padding-block: 8rem; } - .home-contact__panel { padding: 3rem; } +.home-page a { + color: inherit; } -@media (min-width: 64rem) { - .home-hero__layout { grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr); gap: 5rem; padding-block: 6rem; } - .home-hero__title { font-size: clamp(4rem, 7.5vw, 7.25rem); } - .home-workspace { justify-self: end; } - .home-section { padding-block: 9rem; } - .home-section__intro { grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr); } - .home-section__intro > .home-section__copy { justify-self: end; } - .home-work-card__layout { grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr); align-items: center; } - .home-capabilities__layout { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); } - .home-capabilities__intro { position: sticky; top: 7rem; align-self: start; } - .home-style__layout { grid-template-columns: repeat(12, minmax(0, 1fr)); } - .home-style__intro { grid-column: span 5; } - .home-style__cards { grid-column: span 7; } - .home-journal-card { grid-template-columns: minmax(0, 1fr) auto; align-items: end; padding: 4rem; } - .home-contact { padding-block: 10rem; } - .home-contact__panel { padding: 4rem; } - .home-contact__layout { grid-template-columns: minmax(0, 1fr) auto; align-items: end; } -} - -/* Responsive content and reusable tooling. No autoplaying essential content. */ -.home-capability-list { +.editorial-kicker { + color: var(--color-text-muted); + font-size: 0.72rem; + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.editorial-kicker--dark { + color: var(--color-text-muted); +} + +.editorial-hero { + padding-block: clamp(5rem, 9vw, 8rem) clamp(4.5rem, 8vw, 7rem); +} + +.editorial-hero__grid { + display: grid; + gap: clamp(3rem, 7vw, 7rem); + align-items: center; +} + +.editorial-hero__identity { + display: grid; + gap: 2rem; + align-items: center; +} + +.editorial-hero__copy { + max-width: 28rem; +} + +.editorial-hero__name { + margin-top: 0.5rem; + font-size: clamp(2rem, 4vw, 3.15rem); + font-weight: 500; + line-height: 1; + letter-spacing: -0.045em; +} + +.editorial-hero__role { + margin-top: 0.75rem; + color: var(--color-text-muted); + font-size: clamp(1.25rem, 2.3vw, 1.75rem); + font-weight: 400; + letter-spacing: -0.025em; +} + +.editorial-hero__intro { + margin-top: 1.55rem; + color: var(--color-text-secondary); + font-size: 1rem; + line-height: 1.52; +} + +.editorial-hero__links { display: flex; flex-wrap: wrap; + gap: 0.6rem 1.2rem; + margin-top: 1.75rem; + color: var(--color-text-muted); + font-size: 0.78rem; +} + +.editorial-hero__links a, +.editorial-text-link, +.editorial-about__links a { + text-decoration: none; + text-underline-offset: 0.2em; +} + +.editorial-hero__links a:hover, +.editorial-text-link:hover, +.editorial-about__links a:hover { + color: var(--color-accent); +} + +.editorial-feature { + display: grid; + min-height: 13.5rem; + gap: var(--space-6); + align-items: center; + padding: clamp(1.4rem, 3vw, 2.2rem); + border: 1px solid var(--color-border); + background: var(--color-surface); + text-decoration: none; + transition: + transform var(--duration-normal) var(--ease-fluid), + border-color var(--duration-fast) var(--ease-standard); +} + +.editorial-feature:hover { + transform: translateY(-0.2rem); + border-color: var(--color-accent); +} + +.editorial-feature__title { + margin-top: 0.85rem; + font-size: clamp(1.4rem, 2.4vw, 2rem); + font-weight: 600; + letter-spacing: -0.035em; +} + +.editorial-feature__copy { + max-width: 31rem; + margin-top: 0.55rem; + color: var(--color-text-secondary); + font-size: 0.88rem; + line-height: 1.45; +} + +.editorial-feature__media { + display: block; + min-width: 0; + overflow: hidden; + aspect-ratio: 16 / 9; + border: 1px solid var(--color-border-subtle); + background: var(--color-background-strong); +} + +.editorial-feature__media img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: top left; +} + +.editorial-arrow { + display: inline-flex; + align-items: center; + gap: 1rem; + margin-top: 2.5rem; + color: var(--color-accent); + font-size: 0.78rem; +} + +.editorial-feature:hover .editorial-arrow span, +.editorial-project:hover .editorial-project__arrow { + color: var(--color-accent); + transform: translate(0.14rem, -0.14rem); +} + +.editorial-work, +.editorial-strip, +.editorial-about { + border-top: 1px solid var(--color-border); +} + +.editorial-work { + padding-block: clamp(2.75rem, 6vw, 5.5rem) clamp(5.5rem, 9vw, 8rem); +} + +.editorial-section-head { + display: flex; + align-items: start; justify-content: space-between; - gap: 0.75rem 1.5rem; - color: var(--color-mist-300); - font-size: 0.75rem; + gap: 2rem; +} + +.editorial-section-title { + margin-top: 1.5rem; + font-size: clamp(2rem, 4.7vw, 4rem); + font-weight: 400; + line-height: 1.02; + letter-spacing: -0.05em; +} + +.editorial-text-link { + flex-shrink: 0; + margin-top: 0.75rem; + color: var(--color-text-muted); + font-size: 0.78rem; +} + +.editorial-project-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: clamp(1.5rem, 4vw, 3.5rem); + margin-top: clamp(3rem, 6vw, 5rem); +} + +.editorial-project, +.editorial-project:visited { + min-width: 0; + text-decoration: none; +} + +.editorial-project__media { + position: relative; + overflow: hidden; + aspect-ratio: 16 / 10; + background: var(--color-background-strong); +} + +.editorial-project__media img, +.editorial-project__media picture { + display: block; + width: 100%; + height: 100%; +} + +.editorial-project__media img { + object-fit: cover; + transition: transform 450ms var(--ease-fluid); +} + +.editorial-project:hover .editorial-project__media img { + transform: scale(1.018); +} + +.editorial-project__body { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1.25rem; + align-items: start; + padding-top: 1rem; +} + +.editorial-project__meta { + color: var(--color-text-muted); + font-size: 0.7rem; + letter-spacing: 0.035em; + text-transform: uppercase; +} + +.editorial-project__body h3 { + margin-top: 0.35rem; + font-size: clamp(1.15rem, 2vw, 1.55rem); font-weight: 600; - letter-spacing: 0.06em; + letter-spacing: -0.035em; +} + +.editorial-project__body p:not(.editorial-project__meta) { + max-width: 36rem; + margin-top: 0.45rem; + color: var(--color-text-muted); + font-size: 0.83rem; + line-height: 1.45; +} + +.editorial-project__arrow { + color: var(--color-text-muted); + font-size: 1rem; + transition: + transform var(--duration-fast) var(--ease-standard), + color var(--duration-fast) var(--ease-standard); +} + +.editorial-project-stack { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 2rem; +} + +.editorial-project--compact .editorial-project__media { + aspect-ratio: 16 / 6; } -.home-work-card__meta { flex-wrap: wrap; } -.home-work-card + .home-work-card { margin-top: 2rem; } -.home-work-card--workspace { border-color: var(--color-accent-border); } -.home-work-card__copy, .home-hero__copy { min-width: 0; } -.home-workspace__label, .home-workspace__step-copy, .home-work-card__year { +.editorial-project__media--workspace img { + object-position: center 22%; +} + +.editorial-project__media--code, +.editorial-project__media--terminal { + display: grid; + align-content: center; + gap: 0.55rem; + padding: clamp(1.4rem, 3vw, 2.2rem); + font-family: var(--font-mono); + font-size: clamp(0.72rem, 1.2vw, 0.9rem); +} + +.editorial-project__media--code { + background: var(--color-surface); + color: var(--color-text-muted); +} + +.editorial-project__media--code span:nth-child(2) { + color: var(--color-accent); +} + +.editorial-project__media--terminal { + background: var(--color-ink-950); color: var(--color-mist-300); } -.home-system-preview { - padding: clamp(1.25rem, 3vw, 2.5rem); - border: 1px solid var(--color-accent-border); - border-radius: 1.75rem; - background: linear-gradient(135deg, var(--color-ink-850), var(--color-ink-950)); -} -.home-system-preview__grid { display: grid; gap: 1rem; margin-top: 1.5rem; } -.home-system-preview__grid > div { padding: 1.25rem; border: 1px solid var(--color-border-strong); border-radius: 1rem; } -.home-system-preview dt { color: var(--color-mist-50); font-weight: 600; } -.home-system-preview dd { margin-top: 0.5rem; color: var(--color-mist-300); font-size: 0.875rem; } -.home-system-preview__foundation { margin-top: 1.5rem; color: var(--color-signal-200); font-size: 0.8125rem; line-height: 1.75; } - -.home-tooling { padding-block: clamp(4rem, 6vw, 6rem); } -.home-tooling__grid { display: grid; gap: 1.25rem; margin-top: 2.5rem; } -.home-tool-card { min-width: 0; padding: 1.5rem; border: 1px solid var(--color-border-strong); border-radius: 1.5rem; background: var(--color-background-elevated); } -.home-tool-card h3 { margin-top: 1rem; color: var(--color-mist-50); font-size: 1.25rem; overflow-wrap: anywhere; } -.home-tool-card h3 a { display: inline-block; padding-block: 0.5rem; } -.home-tool-card h3 a, .home-tool-card__links a { text-decoration: underline; text-underline-offset: 0.2em; text-decoration-thickness: 1px; } -.home-tool-card p:not(.eyebrow) { margin-top: 0.875rem; color: var(--color-mist-300); font-size: 0.9375rem; line-height: 1.75; } -.home-tool-card pre { margin-top: 1.25rem; padding: 1rem; border-radius: 0.75rem; background: var(--color-ink-950); color: var(--color-signal-200); font-size: 0.875rem; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; } -.home-tool-card__links { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin-top: 1rem; } -.home-tool-card__links a { display: inline-flex; align-items: center; min-height: 2.75rem; color: var(--color-signal-200); font-size: 0.875rem; } -.home-tooling__all { margin-top: 1.5rem; } - -@media (min-width: 30rem) { - .home-system-preview__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.editorial-project__terminal-ok { + color: var(--color-electric-300); +} + +.editorial-strip, +.editorial-about { + padding-block: clamp(4.5rem, 8vw, 7rem); +} + +.editorial-strip__grid, +.editorial-about__grid { + display: grid; + gap: clamp(3rem, 7vw, 7rem); +} + +.editorial-capabilities { + border-top: 1px solid var(--color-border); +} + +.editorial-capabilities a { + display: grid; + grid-template-columns: 2.25rem minmax(0, 1fr) auto; + gap: 1rem; + align-items: center; + padding-block: 1.4rem; + border-bottom: 1px solid var(--color-border); + text-decoration: none; } + +.editorial-capabilities a > span:first-child { + color: var(--color-text-muted); + font-size: 0.7rem; +} + +.editorial-capabilities strong { + font-size: clamp(1rem, 2vw, 1.3rem); + font-weight: 500; +} + +.editorial-capabilities a:hover strong, +.editorial-capabilities a:hover span:last-child { + color: var(--color-accent); +} + +.editorial-about__statement { + max-width: 50rem; + font-size: clamp(1.7rem, 4vw, 3.2rem); + font-weight: 400; + line-height: 1.08; + letter-spacing: -0.045em; +} + +.editorial-about__links { + display: flex; + flex-wrap: wrap; + gap: 0.8rem 1.5rem; + align-self: end; + color: var(--color-text-muted); + font-size: 0.78rem; +} + +@media (min-width: 40rem) { + .editorial-feature { + grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); + } + + .editorial-feature__media { + aspect-ratio: 4 / 3; + } +} + @media (min-width: 48rem) { - .home-tooling__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .editorial-project--compact { + display: grid; + grid-template-columns: minmax(12rem, 0.82fr) minmax(0, 1.18fr); + gap: 1.25rem; + align-items: start; + } + + .editorial-project--compact .editorial-project__media { + aspect-ratio: 16 / 10; + } + + .editorial-project--compact .editorial-project__body { + padding-top: 0.2rem; + } } -@media (max-width: 29.999rem) { - .home-workspace__heading { flex-wrap: wrap; } - .home-workspace__cards { grid-template-columns: 1fr; } - .home-workspace__flow { grid-column: auto; } - .home-work-card, .home-contact__panel { border-radius: 1.5rem; } - .home-contact__panel { padding: 1.25rem; } - .home-contact__title { font-size: clamp(2.25rem, 10vw, 3rem); line-height: 1.05; } + +@media (min-width: 64rem) { + .editorial-hero__grid { + grid-template-columns: minmax(0, 1.13fr) minmax(22rem, 0.87fr); + } + + .editorial-project-grid { + grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); + align-items: start; + } + + .editorial-project--lead { + position: sticky; + top: 7rem; + } + + .editorial-strip__grid { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + align-items: start; + } + + .editorial-about__grid { + grid-template-columns: 0.38fr minmax(0, 1.4fr) auto; + align-items: start; + } } -.home-work-preview__image-frame picture { display: block; width: 100%; } +@media (max-width: 47.999rem) { + .editorial-section-head { + display: grid; + } + + .editorial-text-link { + margin-top: 0; + } + + .editorial-feature { + min-height: 12rem; + } + + /* Text previews need height for their contents, not a 16:6 minimum width. */ + .editorial-project--compact .editorial-project__media--code, + .editorial-project--compact .editorial-project__media--terminal { + aspect-ratio: auto; + min-height: 10rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .editorial-feature, + .editorial-project__media img, + .editorial-project__arrow { + transition: none; + } +} diff --git a/web/static/css/vanilla/shell.css b/web/static/css/vanilla/shell.css index 1385d84..7317854 100644 --- a/web/static/css/vanilla/shell.css +++ b/web/static/css/vanilla/shell.css @@ -7,394 +7,271 @@ .site-shell { position: relative; isolation: isolate; - display: flex; min-height: 100dvh; flex-direction: column; - overflow-x: clip; - - background: - radial-gradient(circle at 10% -8%, color-mix(in oklab, var(--color-signal) 17%, transparent), transparent 35rem), - radial-gradient(circle at 92% 10%, color-mix(in oklab, var(--color-electric) 11%, transparent), transparent 32rem), - linear-gradient(180deg, var(--color-background-panel), var(--color-background) 58%, var(--color-background-panel)); - + background: var(--color-background); color: var(--color-text-primary); } -.site-shell::before { - position: fixed; - inset: 0; - z-index: -2; - - pointer-events: none; - - content: ""; - opacity: 0.11; - - background-image: - linear-gradient(rgb(255 255 255 / 0.07) 1px, transparent 1px), - linear-gradient(90deg, rgb(255 255 255 / 0.07) 1px, transparent 1px); - background-size: 5rem 5rem; - - mask-image: linear-gradient(to bottom, black, transparent 74%); -} - -.site-shell::after { - position: fixed; - inset: 0; - z-index: -1; - - pointer-events: none; - - content: ""; - opacity: 0.022; - - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.92' numOctaves='3' stitchTiles='stitchTiles'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.8'/%3E%3C/svg%3E"); -} - -.site-main { - flex: 1; -} - -.htmx-indicator { - opacity: 0; - visibility: hidden; - - transition: - opacity var(--duration-fast) var(--ease-standard), - visibility var(--duration-fast) var(--ease-standard); -} +.site-main { flex: 1; } +.htmx-indicator { opacity: 0; visibility: hidden; } .htmx-request .htmx-indicator, -.htmx-request.htmx-indicator { - opacity: 1; - visibility: visible; -} +.htmx-request.htmx-indicator { opacity: 1; visibility: visible; } .skip-link { position: fixed; inset-block-start: 0.75rem; inset-inline-start: 0.75rem; z-index: var(--z-overlay); - translate: 0 -180%; - border-radius: var(--radius-pill); background: var(--color-accent-light); - padding: 0.75rem 1rem; - color: var(--color-text-inverse); font-size: var(--text-sm); font-weight: var(--font-bold); - transition: translate var(--duration-fast) var(--ease-standard); } - -.skip-link:focus { - translate: 0; -} +.skip-link:focus { translate: 0; } .page-progress { position: fixed; inset-inline: 0; inset-block-start: 0; z-index: var(--z-overlay); - height: 0.125rem; transform-origin: left center; - - background: var(--color-accent-light); - box-shadow: 0 0 1.375rem rgb(96 165 250 / 0.68); + background: var(--color-signal-400); + box-shadow: 0 0 1.25rem color-mix(in oklab, var(--color-signal-400) 55%, transparent); } -/* -------------------------------------------------------------------------- - HEADER - -------------------------------------------------------------------------- */ - +/* Header */ .site-header { position: sticky; top: 0; z-index: var(--z-header); - + min-height: var(--site-header-height); border-bottom: 1px solid var(--color-border); - - background: rgb(5 11 24 / 0.82); - - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); + background: var(--color-shell-background); } .primary-nav { - display: flex; - min-height: 5rem; + position: relative; + display: grid; + min-height: var(--site-header-height); + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + gap: 1rem; +} +.nav-status { + display: inline-flex; + width: fit-content; align-items: center; - justify-content: space-between; + gap: 0.7rem; + color: var(--color-text-muted); + font-size: 0.68rem; +} - gap: var(--space-5); +.nav-status__dot { + width: 0.34rem; + height: 0.34rem; + border-radius: 50%; + background: var(--color-status-available); + box-shadow: 0 0 0.8rem color-mix(in oklab, var(--color-status-available) 38%, transparent); } .brand-link { - display: inline-flex; - flex-shrink: 0; - align-items: center; + display: grid; + width: 4rem; + height: 4rem; + align-self: start; + place-items: center; + margin-bottom: -1rem; + border-radius: 0 0 999px 999px; + background: var(--color-shell-background); } .brand-logo { - width: auto; - height: 2.25rem; + width: 2rem; + height: auto; } .desktop-nav { display: none; + justify-self: end; align-items: center; - gap: var(--space-7); + gap: 1.25rem; } -.nav-link { - color: var(--color-text-subtle); - font-size: var(--text-sm); - +.nav-link, +.header-project-link { + color: var(--color-text-muted); + font-size: 0.68rem; + text-decoration: none; transition: color var(--duration-fast) var(--ease-standard); } -.nav-link:hover { - color: var(--color-text-primary); -} - -.nav-actions { - display: flex; - align-items: center; - gap: 0.625rem; -} +.nav-link:hover, +.header-project-link:hover { color: var(--color-accent-light); } .header-project-link { - display: none; - min-height: 2.5rem; - padding-inline: 1rem; -} - -.mobile-nav { - position: relative; + padding: 0.45rem 0.95rem; + border: 1px solid var(--color-border-strong); + border-radius: 0.5rem; } +.mobile-nav { justify-self: end; position: relative; } .mobile-nav summary { display: grid; width: 2.5rem; height: 2.5rem; - list-style: none; place-items: center; - - border: 1px solid rgb(255 255 255 / 0.12); - border-radius: var(--radius-pill); - - background: rgb(255 255 255 / 0.04); - color: var(--color-text-secondary); -} - -.mobile-nav summary::-webkit-details-marker { - display: none; -} - -.mobile-nav-icon { - display: grid; - gap: 0.375rem; -} - -.mobile-nav-icon span { - width: 1rem; - height: 1px; - - background: currentColor; - - transition: transform var(--duration-fast) var(--ease-standard); -} - -.mobile-nav[open] .mobile-nav-icon span:first-child { - transform: translateY(0.21875rem) rotate(45deg); -} - -.mobile-nav[open] .mobile-nav-icon span:last-child { - transform: translateY(-0.21875rem) rotate(-45deg); + border: 1px solid var(--color-border-strong); + border-radius: 999px; + background: var(--color-surface); + color: var(--color-text-primary); } - +.mobile-nav summary::-webkit-details-marker { display: none; } +.mobile-nav-icon { display: grid; gap: 0.35rem; } +.mobile-nav-icon span { width: 1rem; height: 1px; background: currentColor; } +.mobile-nav[open] .mobile-nav-icon span:first-child { transform: translateY(0.18rem) rotate(45deg); } +.mobile-nav[open] .mobile-nav-icon span:last-child { transform: translateY(-0.18rem) rotate(-45deg); } .mobile-nav-panel { position: absolute; top: 3rem; right: 0; + width: min(18rem, calc(100vw - 2.5rem)); + padding: 0.75rem; + border: 1px solid var(--color-border-strong); + border-radius: 1rem; + background: var(--color-background-elevated); + box-shadow: var(--shadow-lg); +} +.mobile-nav-links { display: grid; gap: 0.15rem; } +.mobile-nav-link, +.mobile-nav-cta { + padding: 0.8rem 0.9rem; + border-radius: 0.7rem; + color: var(--color-text-muted); + font-size: 0.85rem; + text-decoration: none; +} +.mobile-nav-link:hover { background: var(--color-surface-hover); color: var(--color-text-primary); } +.mobile-nav-cta { margin-top: 0.35rem; background: var(--color-accent); color: var(--color-text-inverse); text-align: center; } + +/* The existing homepage contact and footer share one dark viewport. + A minimum height (not a fixed height) lets zoomed or wrapped text grow. */ +.site-ending { + background: var(--color-shell-background); + color: var(--color-text-primary); +} - width: 16rem; - - padding: var(--space-3); - border-radius: var(--radius-2xl); +.site-ending--home { + display: flex; + min-height: calc(100svh - var(--site-header-height)); + min-height: calc(100dvh - var(--site-header-height)); + flex-direction: column; } -.mobile-nav-links { +.editorial-contact { display: grid; - gap: var(--space-1); + flex: 1; + align-items: center; + scroll-margin-top: var(--site-header-height); + background: var(--color-shell-background); + color: var(--color-text-primary); } -.mobile-nav-link { - border-radius: var(--radius-xl); - padding: 0.75rem 1rem; - - color: var(--color-text-secondary); - font-size: var(--text-sm); - - transition: - background-color var(--duration-fast) var(--ease-standard), - color var(--duration-fast) var(--ease-standard); +.editorial-contact__inner { + display: grid; + gap: 1.5rem; + padding-block: clamp(5rem, 9vw, 8rem); } -.mobile-nav-link:hover { - background: var(--color-surface-hover); - color: var(--color-white); +.editorial-contact h2 { + max-width: 50rem; + font-size: clamp(2.5rem, 6vw, 5rem); + font-weight: 400; + line-height: 0.98; + letter-spacing: -0.055em; } -.mobile-nav-cta { - margin-top: var(--space-2); - border-radius: var(--radius-xl); - padding: 0.75rem 1rem; - - background: var(--color-accent); - color: var(--color-white); - - font-size: var(--text-sm); - font-weight: var(--font-bold); - text-align: center; +.editorial-contact a { + display: inline-flex; + width: fit-content; + min-height: 2.75rem; + align-items: center; + gap: 1rem; + margin-top: 1rem; + color: var(--color-accent); + font-size: 0.9rem; + text-decoration: none; + text-underline-offset: 0.2em; } -/* -------------------------------------------------------------------------- - FOOTER - -------------------------------------------------------------------------- */ +.editorial-contact a:hover { + color: var(--color-accent-light); + text-decoration: underline; +} +/* Footer — the removed duplicate CTA stays removed. */ .site-footer { position: relative; z-index: var(--z-raised); - + flex-shrink: 0; margin-top: auto; border-top: 1px solid var(--color-border); - - background: rgb(3 8 20 / 0.38); -} - -.footer-inner { - padding-block: var(--space-12); -} - -.footer-cta { - display: grid; - gap: var(--space-10); - - border-bottom: 1px solid var(--color-border); - padding-bottom: var(--space-12); -} - -.footer-title { - margin-top: var(--space-6); - max-width: var(--content-xl); - - color: var(--color-text-primary); - - font-size: clamp(2.25rem, 5vw, 3.75rem); - font-weight: var(--font-medium); - line-height: var(--leading-heading); - letter-spacing: var(--tracking-display); - text-wrap: balance; + background: var(--color-shell-background); } +.site-ending--home .site-footer { border-top: 0; } +.footer-inner { padding-block: var(--space-8); } .gradient-text { - background: linear-gradient(90deg, var(--color-accent-light), var(--color-electric)); + background: linear-gradient(90deg, var(--color-signal-300), var(--color-electric-300)); background-clip: text; -webkit-background-clip: text; color: transparent; } - -.footer-contact-button { - width: fit-content; -} - .footer-meta { display: grid; gap: var(--space-8); - - padding-top: var(--space-8); - - color: var(--color-text-subtle); + color: var(--color-text-muted); font-size: var(--text-sm); } - -.footer-logo-link { - display: inline-flex; - width: fit-content; - align-items: center; -} - -.footer-logo { - width: auto; - height: 2.25rem; -} - -.footer-links { - display: flex; - flex-wrap: wrap; - gap: 0.75rem var(--space-6); -} - -.footer-link { - transition: color var(--duration-fast) var(--ease-standard); -} - -.footer-link:hover { - color: var(--color-text-primary); -} - -.footer-copyright { - font-size: var(--text-xs); -} - +.footer-logo-link { display: inline-flex; width: fit-content; align-items: center; } +.footer-logo { width: auto; height: 2.25rem; } +.footer-links { display: flex; flex-wrap: wrap; gap: 0.75rem var(--space-6); } +.footer-link:hover { color: var(--color-accent-light); } +.footer-copyright { color: var(--color-text-muted); font-size: var(--text-xs); } @media (min-width: 40rem) { - .brand-logo { - height: 2.5rem; - } - - .header-project-link { - display: inline-flex; - } - - .footer-inner { - padding-block: var(--space-16); - } - - .footer-meta { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } + .footer-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (min-width: 64rem) { - .desktop-nav { - display: flex; - } - - .mobile-nav { - display: none; - } - - .footer-cta { - grid-template-columns: minmax(0, 1fr) auto; - align-items: end; - } - - .footer-meta { - grid-template-columns: minmax(0, 1fr) auto auto; - align-items: center; - } - - .footer-copyright { - text-align: right; + .desktop-nav { display: flex; } + .mobile-nav { display: none; } + .footer-meta { grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; } + .footer-copyright { text-align: right; } +} + +@media (max-width: 39.999rem) { + /* Keep the compact dot, but do not remove its meaning from screen readers. */ + .nav-status__text { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; } + .brand-link { width: 3.75rem; height: 3.75rem; } } diff --git a/web/static/css/vanilla/tokens.css b/web/static/css/vanilla/tokens.css index 831c44b..7f959ed 100644 --- a/web/static/css/vanilla/tokens.css +++ b/web/static/css/vanilla/tokens.css @@ -7,8 +7,7 @@ :root { /* ---------------------------------------------------------------------- COLOUR — FOUNDATIONS - Mirrors the existing Tailwind theme while the vanilla CSS migration is - in progress. These values are the visual source of truth. + Shared brand palette. These values are the visual source of truth. ---------------------------------------------------------------------- */ --color-ink-950: oklch(0.12 0.032 257); @@ -26,6 +25,8 @@ --color-signal-300: oklch(0.83 0.095 241); --color-signal-400: oklch(0.75 0.135 244); --color-signal-500: oklch(0.66 0.17 248); + /* Same brand hue, deepened for readable links on light surfaces. */ + --color-signal-700: oklch(0.5 0.17 248); --color-electric-300: oklch(0.88 0.055 215); --color-electric-400: oklch(0.8 0.095 221); @@ -44,6 +45,8 @@ --color-background-elevated: var(--color-ink-900); --color-background-panel: var(--color-ink-850); --color-background-strong: var(--color-ink-800); + --color-shell-background: var(--color-black); + --color-status-available: var(--color-signal-400); --color-text-primary: var(--color-mist-50); --color-text-secondary: var(--color-mist-100); @@ -179,6 +182,7 @@ --container-width: 82rem; --container-gutter: clamp(1.25rem, 6vw, 3rem); + --site-header-height: 3.5rem; --content-xs: 24rem; --content-sm: 32rem; @@ -276,3 +280,34 @@ --focus-ring: 0 0 0 3px color-mix(in oklab, var(--color-signal-300) 30%, transparent); } + +/* Light content opts in without changing the journal, services or workspace. + Override the semantic tokens so base headings, paragraphs and strong text + resolve to the correct colours, rather than relying on inherited `color`. */ +[data-theme="light"] { + color-scheme: light; + + --color-background: var(--color-mist-100); + --color-background-elevated: var(--color-mist-50); + --color-background-panel: var(--color-mist-50); + --color-background-strong: color-mix(in oklab, var(--color-mist-100) 85%, var(--color-mist-300)); + + --color-text-primary: var(--color-ink-950); + --color-text-secondary: var(--color-ink-800); + --color-text-muted: var(--color-mist-700); + --color-text-subtle: var(--color-mist-700); + --color-text-faint: var(--color-mist-700); + --color-text-inverse: var(--color-mist-50); + + --color-accent: var(--color-signal-700); + --color-accent-light: var(--color-signal-700); + --color-accent-strong: var(--color-signal-700); + --color-border: color-mix(in oklab, var(--color-ink-950) 18%, transparent); + --color-border-subtle: color-mix(in oklab, var(--color-ink-950) 10%, transparent); + --color-border-strong: color-mix(in oklab, var(--color-ink-950) 28%, transparent); + --color-surface: var(--color-mist-50); + --color-surface-hover: var(--color-signal-200); + --color-accent-surface: color-mix(in oklab, var(--color-signal-700) 8%, transparent); + --color-accent-border: color-mix(in oklab, var(--color-signal-700) 35%, transparent); + --focus-ring: 0 0 0 3px var(--color-signal-700); +} diff --git a/web/static/images/daniel-avatar.svg b/web/static/images/daniel-avatar.svg new file mode 100644 index 0000000..d41a554 --- /dev/null +++ b/web/static/images/daniel-avatar.svg @@ -0,0 +1,5 @@ + +Daniel Manning avatar +Illustrated portrait of Daniel Manning. + + diff --git a/web/templates/components/footer.html b/web/templates/components/footer.html index f22ed0c..38f9ebb 100644 --- a/web/templates/components/footer.html +++ b/web/templates/components/footer.html @@ -1,42 +1,39 @@ {{define "footer"}} - + + {{end}} diff --git a/web/templates/components/header.html b/web/templates/components/header.html index b7ede60..c821a0d 100644 --- a/web/templates/components/header.html +++ b/web/templates/components/header.html @@ -1,13 +1,18 @@ {{define "header"}} {{end}} diff --git a/web/templates/pages/public/home.html b/web/templates/pages/public/home.html index e465dca..a04724f 100644 --- a/web/templates/pages/public/home.html +++ b/web/templates/pages/public/home.html @@ -1,384 +1,169 @@ {{define "content"}} -
-
- - -
-
-
- -

Digital Product Designer & Developer

-
- -

- Building - digital products - that matter. -

- -

- I turn complicated workflows into focused digital products, combining thoughtful UI/UX with fast, - secure full-stack engineering. -

- - - -
-
-
Focus
-
Product clarity
-
-
-
Discipline
-
Design + engineering
-
-
-
Core stack
-
Go / HTMX / SQLite
-
-
-
- -
- - -
-
- -

product workspace

-
- -
-
-
-

Design principle

-

Make every next step obvious.

-
- - - Focused - -
- -
-
-

Interface

-

Premium, not precious

-

Strong hierarchy, accessible states and restrained - motion.

-
- -
-

Engineering

-

Fast, not fragile

-

Small layers and server-rendered interactions.

-
+
+
+
+
+
+

Hello, I’m

+

Daniel Manning

+

Designer & Developer

+

+ I turn complicated workflows into focused digital products, combining thoughtful UI/UX with fast, + secure full-stack engineering. +

-
-
-

Product flow

- LIVE -
-
- 01 -
-

Understand the decision

-

Research the real friction.

-
- 02 -
-

Shape the experience

-

Make information and interaction coherent. -

-
- 03 -
-

Build the product

-

Ship a dependable working system.

-
-
-
-
+
-
-
-
+
-
+
-

01 / Selected work

-

- Product thinking you can - inspect. -

+

Selected work

+

Projects that make
a difference.

-

Two different challenges. One connected approach to design and development.

+ View all work
- -
-
-
- Featured case study - 2026 -
- -

- Salon Rebuild - - Design with a clearer purpose - -

- -

- A modern salon concept revisiting my early freelance work with a more considered UI/UX, - responsive design and server-rendered Go implementation. -

- -
- Redesign - UI/UX - Go - Server-rendered HTML -
- - +
+ -
-
-
- -

Small, focused tools I use to reduce repeated setup and keep implementation consistent.

-
-
-
-

Developer workflow

-

djm-cli

-

Create a Go project, add a module and run the development checks from one command-line tool.

-
djm new my-app
-cd my-app
-djm check
-
-
-

Application foundation

-

Go Web Stack

-

Independent libraries for configuration, rendering, security and authentication, brought together in a starter.

- -
-
-

Structured data

-

go-jsonld-schema

-

Typed Go structures for Schema.org JSON-LD, without hand-writing JSON inside HTML templates.

-

Reusable types for websites, articles, services and breadcrumbs.

-
-
- View all projects on GitHub -
-
- -
-
-
-

03 / Capabilities

-

- One accountable loop from idea to - release. -

-

Design and engineering decisions stay connected, so the - interface, system and product story reinforce each other.

-
- -
-
- 01 -
-

Product & UX strategy

-

Clarify the outcome, map the workflow and decide what the - product should make easy.

-
-
-
- 02 -
-

Interface systems

-

Accessible layouts, components, states and content hierarchy - that remain coherent as the product grows.

-
-
-
- 03 -
-

Full-stack product engineering

-

Fast server-rendered applications, database workflows and - secure operational tooling without unnecessary frontend weight.

-
-
-
-
-
-

04 / Working style

-

Close to the work. Clear about the trade-offs.

-

I follow an idea from the rough first question through interface - design, implementation and the details that make it trustworthy in use.

+
+
+
+

Capabilities

+

From product thinking
to working software.

- -
-
-

01

-

Frame

-

Define the real user decision, constraints and evidence of success. -

-
-
-

02

-

Shape

-

Design the information, interaction and technical boundaries - together.

-
-
-

03

-

Ship

-

Build, test, harden and refine the moments that affect confidence. -

-
+
-
- -
- -
-
-
- -
-
-

06 / Contact

-

- Have something - worth building? -

-
-
-

Bring the difficult workflow, the early idea or the product that - needs to become much clearer.

- Start - a conversation -
-
+
+
+

About

+

+ I design interfaces and build the systems behind them. My preference is simple: less visual noise, + clearer decisions and software that stays fast and maintainable. +

+