diff --git a/index.html b/index.html index 81ff06b..6689bd9 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,10 @@ + + + + diff --git a/src/components/sections/Footer.scss b/src/components/sections/Footer.scss index 6980484..88ba871 100644 --- a/src/components/sections/Footer.scss +++ b/src/components/sections/Footer.scss @@ -12,15 +12,11 @@ gap: $space-md; padding-block: $space-lg; - @media (min-width: 1100px) { - flex-direction: row; - flex-wrap: wrap; - align-items: center; - gap: $space-lg $space-xl; - } - @include desktop-up { - grid-template-columns: 1fr auto auto auto 1fr; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + grid-template-areas: "brand copyright socials"; + align-items: center; + column-gap: clamp(1.5rem, 4vw, 3rem); } } @@ -28,6 +24,11 @@ display: flex; flex-direction: column; gap: 0.25rem; + + @include desktop-up { + grid-area: brand; + justify-self: start; + } } &__wordmark { @@ -96,12 +97,14 @@ &__meta { display: flex; align-items: center; + justify-content: center; gap: $space-md; + text-align: center; - @media (min-width: 1100px) { - flex: 0 0 auto; - flex-direction: column; - gap: 0.4rem; + @include desktop-up { + grid-area: copyright; + align-self: center; + justify-self: center; } } @@ -111,6 +114,8 @@ gap: 0.625rem; @include desktop-up { + grid-area: socials; + justify-self: end; justify-content: flex-end; } diff --git a/src/components/sections/Navbar.scss b/src/components/sections/Navbar.scss index c1b1a23..89a7e1e 100644 --- a/src/components/sections/Navbar.scss +++ b/src/components/sections/Navbar.scss @@ -7,15 +7,10 @@ left: 0; right: 0; z-index: $z-fixed; - border-bottom: 1px solid transparent; - background: rgba(1, 24, 28, 0.94); - transition: - background-color $transition-base, - border-color $transition-base; // Blur-only backdrop (no darkening tint) with a feathered bottom edge. // Lives on a pseudo-element so the logo/links stay crisp and unmasked. - &__backdrop { + &::before { content: ""; position: absolute; inset: 0 0 -24px 0; @@ -32,16 +27,12 @@ transition: opacity 320ms ease; } - &--scrolled &__backdrop { + &--scrolled::before { opacity: 1; } - &--scrolled { - border-color: $color-border; - background: rgba(1, 24, 28, 0.98); - } - &__container { + position: relative; @include container; display: flex; align-items: center; @@ -78,8 +69,9 @@ @media (max-width: #{$bp-desktop - 1px}) { position: fixed; inset: 68px 0 0 0; - background: $color-bg; - border-top: 1px solid $color-border; + background: rgba(0, 22, 29, 0.97); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); display: flex; flex-direction: column; align-items: stretch; diff --git a/src/components/sections/Services.scss b/src/components/sections/Services.scss index 937cd39..cb90d69 100644 --- a/src/components/sections/Services.scss +++ b/src/components/sections/Services.scss @@ -1,28 +1,22 @@ @use '../../styles/variables' as *; @use '../../styles/mixins' as *; -#services.section { - padding-bottom: clamp(2.5rem, 4vw, 4rem); - - .sec-head { - margin-bottom: clamp(1.5rem, 2.5vw, 2.25rem); - } -} - .services { &__grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); - gap: clamp(0.9rem, 1.5vw, 1.25rem); - max-width: 54rem; + grid-template-columns: 1fr; + gap: $space-md; + + @include mobile-up { + grid-template-columns: repeat(2, 1fr); + } } &__card { position: relative; @include engineered-panel(30px); - min-height: clamp(11.25rem, 14vw, 12.75rem); - padding: clamp(1.35rem, 2vw, 1.75rem); - padding-right: clamp(4.75rem, 7vw, 6rem); + padding: $space-lg; + padding-right: clamp(5.25rem, 10vw, 7rem); &:hover { border-color: $color-glass-edge; @@ -66,23 +60,21 @@ &__title { @include display; - font-size: clamp(1.45rem, 2vw, 1.65rem); + font-size: $text-2xl; line-height: 1.04; letter-spacing: $tracking-tight; - margin-bottom: 0.5rem; + margin-bottom: 0.65rem; transition: color $transition-base; } &__desc { - margin: 0; - max-width: 36ch; - font-size: clamp(0.92rem, 1.1vw, 0.98rem); - line-height: 1.48; + font-size: $text-sm; + line-height: $leading-normal; color: $color-text-2; } &__marquee { - margin-top: clamp(1.75rem, 3vw, 2.75rem); + margin-top: $space-xl; overflow: hidden; mask-image: linear-gradient(to right, transparent 0, #000 11%, #000 89%, transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0, #000 11%, #000 89%, transparent 100%); @@ -139,7 +131,6 @@ } &__card { - min-height: auto; padding: 1.35rem; padding-right: 4rem; } @@ -161,7 +152,7 @@ } &__marquee { - margin-top: 1.5rem; + margin-top: 2rem; } &__tag { diff --git a/src/components/sections/Team.scss b/src/components/sections/Team.scss index c4fd823..f6810e6 100644 --- a/src/components/sections/Team.scss +++ b/src/components/sections/Team.scss @@ -15,15 +15,11 @@ &__card { position: relative; - border: 1px solid $color-border-2; - border-radius: 30px; - background: $color-surface; - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.06), - 0 12px 36px rgba(0, 0, 0, 0.24); + @include engineered-panel(30px); overflow: hidden; display: flex; flex-direction: column; + scroll-margin-top: 5.75rem; width: 100%; transition: border-color $transition-base, @@ -31,8 +27,9 @@ transform 300ms $ease-spring; &:hover { - border-color: $color-border-2; - background: $color-surface-2; + border-color: $color-glass-edge; + background: $color-glass-2; + transform: translateY(-4px); .team__avatar { filter: grayscale(0) contrast(1); } @@ -50,6 +47,8 @@ &__photo { position: relative; + aspect-ratio: 1 / 1; + overflow: hidden; border-bottom: 1px solid $color-glass-border; } @@ -92,6 +91,7 @@ font-size: clamp(0.82rem, 2.8vw, $text-sm); line-height: 1.48; color: $color-text-2; + flex: 1; } &__github { @@ -124,8 +124,6 @@ } &__github-arrow { - margin-left: 0.15rem; - color: $color-text-3; transition: transform $transition-base, color $transition-base; } } diff --git a/src/components/ui/Button.scss b/src/components/ui/Button.scss index 36520a4..e964494 100644 --- a/src/components/ui/Button.scss +++ b/src/components/ui/Button.scss @@ -1,6 +1,7 @@ @use '../../styles/variables' as *; .btn { + position: relative; display: inline-flex; align-items: center; justify-content: center; diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 696449f..fba1396 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -129,8 +129,8 @@ // Display type (headlines) — Tilt Warp @mixin display { font-family: $font-display; - font-weight: 600; - letter-spacing: $tracking-tight; + font-weight: 400; + letter-spacing: 0; line-height: 1.08; color: $color-text; } diff --git a/src/styles/_reset.scss b/src/styles/_reset.scss index 1929f84..2bef29d 100644 --- a/src/styles/_reset.scss +++ b/src/styles/_reset.scss @@ -17,6 +17,7 @@ html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + font-synthesis-weight: none; text-rendering: optimizeLegibility; } diff --git a/tests/acceptance/content.spec.ts b/tests/acceptance/content.spec.ts index 8664330..7ca31e9 100644 --- a/tests/acceptance/content.spec.ts +++ b/tests/acceptance/content.spec.ts @@ -136,4 +136,44 @@ test.describe('landing page content', () => { await expect(footer.locator(`a[href="${url}"]`)).toBeVisible(); } }); + + test('footer layout keeps copyright centered and socials on the right', async ({ page }) => { + await page.goto('/'); + await page.locator('.footer').scrollIntoViewIfNeeded(); + + const layout = await page.evaluate(() => { + const rect = (selector: string) => { + const element = document.querySelector(selector); + const box = element?.getBoundingClientRect(); + + if (!box) return null; + + return { + left: box.left, + right: box.right, + center: box.left + box.width / 2, + }; + }; + + return { + viewportCenter: window.innerWidth / 2, + viewportWidth: window.innerWidth, + copyright: rect('.footer__copyright'), + socials: rect('.footer__socials'), + firstSocial: rect('.footer__socials .social-icon'), + }; + }); + + expect(layout.copyright).not.toBeNull(); + expect(layout.socials).not.toBeNull(); + expect(layout.firstSocial).not.toBeNull(); + expect(Math.abs((layout.copyright?.center ?? 0) - layout.viewportCenter)).toBeLessThanOrEqual(1); + + if (layout.viewportWidth >= 1024) { + expect(layout.socials?.left).toBeGreaterThan((layout.copyright?.right ?? 0) + 32); + expect(layout.socials?.right).toBeGreaterThanOrEqual(layout.viewportWidth - 96); + } else { + expect(layout.firstSocial?.left).toBeLessThanOrEqual(32); + } + }); }); diff --git a/tests/acceptance/design-baseline.spec.ts b/tests/acceptance/design-baseline.spec.ts index a6f2dd7..67b6d7a 100644 --- a/tests/acceptance/design-baseline.spec.ts +++ b/tests/acceptance/design-baseline.spec.ts @@ -1,5 +1,13 @@ import { expect, type Page, test } from '@playwright/test'; +function alphaFromRgb(color: string) { + const match = color.match(/rgba?\(([^)]+)\)/); + if (!match) return 1; + + const parts = match[1].split(',').map((part) => part.trim()); + return parts.length === 4 ? Number(parts[3]) : 1; +} + async function findRuleText(page: Page, selector: string) { return page.evaluate((targetSelector) => { const matches: string[] = []; @@ -33,17 +41,16 @@ async function waitForScrolledNavbarEffect(page: Page) { await expect(page.locator('.navbar')).toHaveClass(/navbar--scrolled/); await expect .poll(() => - page.locator('.navbar').evaluate((element) => getComputedStyle(element).backgroundColor), + page.locator('.navbar').evaluate((element) => Number(getComputedStyle(element, '::before').opacity)), ) - .not.toBe('rgba(0, 0, 0, 0)'); + .toBeGreaterThan(0.95); } async function waitForVisualReady(page: Page) { - await page.evaluate(() => document.fonts.ready); - // Freeze animated mesh so Linux/Windows captures stay aligned. await page.addStyleTag({ - content: '.App::before { animation: none !important; transform: none !important; }', + content: 'canvas { visibility: hidden !important; } .App__background { animation: none !important; transform: none !important; }', }); + await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(300); } @@ -90,7 +97,7 @@ test.describe('design baseline', () => { await expect(page.locator('.navbar')).toHaveScreenshot(`${testInfo.project.name}-navbar.png`, { animations: 'disabled', caret: 'hide', - maxDiffPixelRatio: 0.02, + maxDiffPixelRatio: 0.03, }); }); @@ -103,81 +110,62 @@ test.describe('design baseline', () => { await expect(page.locator('.navbar')).toHaveScreenshot(`${testInfo.project.name}-navbar-scrolled.png`, { animations: 'disabled', caret: 'hide', - maxDiffPixelRatio: 0.02, + maxDiffPixelRatio: 0.03, }); }); - test('team cards match approved baselines', async ({ page }, testInfo) => { + test('glass surfaces match approved baselines', async ({ page }, testInfo) => { await page.goto('/'); await lockViewportOn(page, '#team'); await waitForVisualReady(page); + await page.addStyleTag({ content: '.navbar { visibility: hidden !important; }' }); - await expect(page.locator('.team__card').first()).toHaveScreenshot(`${testInfo.project.name}-team-card.png`, { + await expect(page.locator('.team__card').first()).toHaveScreenshot(`${testInfo.project.name}-team-glass.png`, { animations: 'disabled', caret: 'hide', - maxDiffPixelRatio: 0.03, + maxDiffPixelRatio: 0.035, }); await page.locator('.team__card').first().hover(); await page.waitForTimeout(300); - await expect(page.locator('.team__card').first()).toHaveScreenshot(`${testInfo.project.name}-team-card-hover.png`, { + await expect(page.locator('.team__card').first()).toHaveScreenshot(`${testInfo.project.name}-team-glass-hover.png`, { animations: 'disabled', caret: 'hide', - maxDiffPixelRatio: 0.03, + maxDiffPixelRatio: 0.035, }); }); - test('solid surfaces are used instead of glass', async ({ page }) => { + test('glass and transparency effects are active', async ({ page }) => { await page.goto('/'); await lockViewportOn(page, '#team'); - const cardStyles = await page.locator('.team__card').first().evaluate((element) => { + const glassStyles = await page.locator('.team__card').first().evaluate((element) => { const style = getComputedStyle(element); return { backgroundColor: style.backgroundColor, - backdropFilter: style.backdropFilter, }; }); const teamCardRule = await findRuleText(page, '.team__card'); - expect(cardStyles.backgroundColor, 'team card should use an opaque surface').toMatch(/rgb/); - expect(cardStyles.backdropFilter, 'team card should not use backdrop blur').toBe('none'); - expect(teamCardRule, 'team card should not ship glass blur rules').not.toContain('backdrop-filter'); + expect(alphaFromRgb(glassStyles.backgroundColor), 'team card should keep a translucent glass background').toBeLessThan(1); + expect(teamCardRule, 'team card should keep a backdrop blur CSS rule').toContain('backdrop-filter'); + expect(teamCardRule, 'team card should keep a backdrop blur CSS rule').toContain('blur'); await page.evaluate(() => window.scrollTo(0, 360)); await waitForScrolledNavbarEffect(page); + const navbarEffect = await page.locator('.navbar').evaluate((element) => { + const style = getComputedStyle(element, '::before'); + return { + opacity: style.opacity, + }; + }); const navbarRule = await findRuleText(page, '.navbar::before'); - expect(navbarRule, 'navbar should not rely on a blur pseudo-layer').toBe(''); - }); - test('team photos keep square framing', async ({ page }) => { - await page.goto('/'); - await lockViewportOn(page, '#team'); - await waitForVisualReady(page); - - const frames = await page.locator('.team__photo').evaluateAll((elements) => - elements.map((element) => { - const frame = element.getBoundingClientRect(); - const image = element.querySelector('img')?.getBoundingClientRect(); - - return { - frameWidth: frame.width, - frameHeight: frame.height, - imageWidth: image?.width ?? 0, - imageHeight: image?.height ?? 0, - }; - }), - ); - - expect(frames).toHaveLength(3); - - for (const frame of frames) { - expect(Math.abs(frame.frameWidth - frame.frameHeight), 'team photo frame should stay square').toBeLessThan(2); - expect(Math.abs(frame.imageWidth - frame.frameWidth), 'team image should fill frame width').toBeLessThan(2); - expect(Math.abs(frame.imageHeight - frame.frameHeight), 'team image should fill frame height').toBeLessThan(2); - } + expect(Number(navbarEffect.opacity), 'scrolled navbar blur layer should be visible').toBeGreaterThan(0.9); + expect(navbarRule, 'scrolled navbar should keep backdrop blur CSS rule').toContain('backdrop-filter'); + expect(navbarRule, 'scrolled navbar should keep backdrop blur CSS rule').toContain('blur'); }); for (const state of states) { @@ -189,7 +177,7 @@ test.describe('design baseline', () => { animations: 'disabled', caret: 'hide', fullPage: false, - maxDiffPixelRatio: 0.025, + maxDiffPixelRatio: 0.03, }); }); } diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-contact.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-contact.png index 5162ffb..ea62450 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-contact.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-contact.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar-scrolled.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar-scrolled.png index 035b085..deb81b1 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar-scrolled.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar-scrolled.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar.png index c5bf23f..5c65e70 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-navbar.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass-hover.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass-hover.png index e971eb8..a04584b 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass-hover.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass-hover.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass.png index bdb09b7..551d310 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team-glass.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team.png b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team.png index 1818f6b..969bbba 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/desktop-team.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-contact.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-contact.png index 00d40e0..00fbc6e 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-contact.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-contact.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar-scrolled.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar-scrolled.png index bc4ef52..2fa9c91 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar-scrolled.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar-scrolled.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar.png index 9f3c770..a179717 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-navbar.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass-hover.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass-hover.png index f786080..d2a5386 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass-hover.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass-hover.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass.png index cfd0eb8..9d779f8 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team-glass.png differ diff --git a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team.png b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team.png index 9600a5e..e7af2f7 100644 Binary files a/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team.png and b/tests/acceptance/design-baseline.spec.ts-snapshots/mobile-team.png differ