diff --git a/site/public/index.html b/site/public/index.html index feda50d0..256d56ad 100644 --- a/site/public/index.html +++ b/site/public/index.html @@ -4,13 +4,13 @@ Featherframe: a picture frame for bird lovers that shows who’s singing nearby - + - + @@ -21,7 +21,7 @@ - + @@ -52,7 +52,7 @@ "@type": "Product", "@id": "https://featherframe.app/#product", "name": "Featherframe", - "description": "Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.", + "description": "A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. A framed e-paper display with no subscription.", "image": ["https://featherframe.app/img/og.jpg"], "brand": { "@id": "https://wells.ee/#org" }, "manufacturer": { "@id": "https://wells.ee/#org" }, @@ -84,8 +84,14 @@

Let the outside in.

-

Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.

-

One email when Featherframe is available, and nothing else.

+

A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s.

+ +

The first batch is just 50 frames. The waitlist hears first. No spam.

diff --git a/site/public/styles.css b/site/public/styles.css index 28b865ce..b2e1b2e8 100644 --- a/site/public/styles.css +++ b/site/public/styles.css @@ -163,6 +163,13 @@ h2 { font-weight: 400; text-wrap: balance; } .stage.live .poster { opacity: 0; transition: opacity .6s ease; } .cover .cta .wl { position: relative; z-index: 2; } .cover .copy > p { font-size: var(--t-lede); line-height: var(--lh-lede); margin: 0 0 26px; text-wrap: pretty; } +/* what a buyer asks first, under the sentence: who makes it, how it sets up, where it works, what it costs. Each with its icon, set + small and upright in the field's ink. */ +.cover .facts { list-style: none; margin: -8px 0 24px; padding: 0; font-size: var(--t-small); line-height: var(--lh-lede); } +.cover .facts li { display: flex; align-items: flex-start; gap: 10px; text-wrap: pretty; } +.cover .facts li + li { margin-top: 6px; } +.cover .facts .icon { margin-top: 0; color: var(--ink); } +.cover .facts .icon .solid { fill: currentColor; } /* Connecticut's heart; the bird's eye and its note */ /* I. The art: a spread, the frame showing the flamingo in its left half, pinned there, centred below the running head, while the text scrolls past. */ @@ -588,7 +595,7 @@ h2 { font-weight: 400; text-wrap: balance; } --W: calc(100vw - 2 * var(--edge)); --V: calc(100svh - var(--nav)); /* under 1000px the sentence is set at the body size, so the frame has room beside it */ - --gnd: 220px; --measure: 400px; + --gnd: 290px; --measure: 400px; --F: clamp(72px, min((var(--W) - .9521 * var(--gnd)) / 5.0466, (var(--W) - var(--measure) - .9521 * var(--gnd)) / 1.2026, (var(--V) - 112px - var(--gnd)) / 1.913), 240px); --ht: calc(1.913 * var(--F) + var(--gnd)); /* the words: the headline's cap line to the note's last baseline */ @@ -614,10 +621,17 @@ h2 { font-weight: 400; text-wrap: balance; } } } @media (max-width: 820px) and (min-aspect-ratio: 1/1) { - .cover { --gnd: 214px; } + .cover { --gnd: 286px; } +} +/* a small landscape window: the facts set tighter, so the words, the headline and the frame still share the screen */ +@media (min-width: 701px) and (max-width: 999px) and (min-aspect-ratio: 1/1) { + .cover .facts { margin: -12px 0 18px; font-size: var(--t-fine); line-height: 1.3; } + .cover .facts li + li { margin-top: 2px; } + .cover .copy > p { margin-bottom: 20px; } + .cover .cta .wl-note { font-size: var(--t-fine); } } @media (min-width: 1000px) and (min-aspect-ratio: 1/1) { - .cover { --gnd: 248px; --measure: calc(17.7 * var(--t-lede)); } + .cover { --gnd: 341px; --measure: calc(17.7 * var(--t-lede)); } .cover .copy > p { font-size: var(--t-lede); } } @media (min-width: 1100px) and (min-aspect-ratio: 1/1) { @@ -627,7 +641,7 @@ h2 { font-weight: 400; text-wrap: balance; } } } @media (min-width: 1200px) and (min-aspect-ratio: 1/1) { - .cover { --gnd: 211px; --measure: calc(22 * var(--t-lede)); } + .cover { --gnd: 305px; --measure: calc(21 * var(--t-lede)); } } /* A portrait window or a tablet: the phone's order, the headline @@ -643,8 +657,8 @@ h2 { font-weight: 400; text-wrap: balance; } --pad: 40px; --g1: calc(.3 * var(--F)); /* the headline's last baseline to the frame's top edge */ --g2: 40px; /* the frame's foot to the sentence */ - --words: 217px; /* the sentence, the waitlist's field and the note under it (200px under 821px) */ - --fh: min(max(.4 * var(--V), var(--V) - 2 * var(--pad) - 1.513 * var(--F) - var(--g1) - var(--g2) - var(--words)), .72 * var(--W) / .843); + --words: 311px; /* the sentence, the facts, the waitlist's field and the note under it (299px under 821px) */ + --fh: min(max(.35 * var(--V), var(--V) - 2 * var(--pad) - 1.513 * var(--F) - var(--g1) - var(--g2) - var(--words)), .72 * var(--W) / .843); --hb: calc(var(--fh) / .7379); --wb: calc(var(--hb) * .78376); display: flex; flex-direction: column; justify-content: center; min-height: var(--V); padding: var(--pad) 0; @@ -664,7 +678,7 @@ h2 { font-weight: 400; text-wrap: balance; } } } @media (min-width: 701px) and (max-width: 820px) and (aspect-ratio < 1) { - .cover { --words: 200px; } + .cover { --words: 299px; } } @media (max-width: 820px) { diff --git a/site/test/site.spec.ts b/site/test/site.spec.ts index 56c5692f..a84ed219 100644 --- a/site/test/site.spec.ts +++ b/site/test/site.spec.ts @@ -13,7 +13,7 @@ test('social card and search basics', async ({ page, request }) => { const product = ld['@graph'].find((n: { '@type': string }) => n['@type'] === 'Product'); expect(product).toMatchObject({ name: 'Featherframe', url: 'https://featherframe.app/' }); expect(product).not.toHaveProperty('offers'); - await expect(page.locator('meta[name="twitter:description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.'); + await expect(page.locator('meta[name="twitter:description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.'); await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', 'https://featherframe.app/'); expect((await request.get('/robots.txt')).ok()).toBe(true); expect((await request.get('/sitemap.xml')).ok()).toBe(true); @@ -68,11 +68,13 @@ test('every section and its key copy is there', async ({ page }) => { await expect(page.locator('#specs .spec dd').nth(6)).toHaveText('Open source. See it on GitHub'); await expect(page.locator('#specs .spec dd').nth(6).getByRole('link', { name: 'See it on GitHub' })).toHaveAttribute('href', 'https://github.com/wr/featherframe'); await expect(page.locator('#specs .spec')).not.toContainText('Service'); - // no price, sale or date anywhere (W-1020): the hero holds the signup alone, and each size is named for its display + // no sale or date anywhere (W-1020); the one price is the hero's "From $349", for buyers who ask what it costs first. + // The signup holds the form alone, and each size is named for its display await expect(page.locator('.cover .cta > *')).toHaveCount(1); await expect(page.locator('#specs .ho figcaption')).toHaveText(['10.3-inch · B&W', '13.3-inch · Color']); await expect(page.locator('s')).toHaveCount(0); - await expect(page.locator('body')).not.toContainText(/Black Friday|November|December|holiday|\$(299|379|349|479)/i); + await expect(page.locator('body')).not.toContainText(/Black Friday|November|December|holiday|\$(299|379|479)/i); + await expect(page.locator('.cover .facts')).toContainText('$349'); await expect(page.locator('#specs .ho .diag span')).toHaveText(['10.3-inch display', '13.3-inch display']); await expect(page.locator('#specs .dim')).toHaveCount(0); // the close says the cover's line again @@ -86,9 +88,10 @@ test('every section and its key copy is there', async ({ page }) => { await expect(page.locator('.wall .folio')).toHaveText('From the collection'); await expect(page.locator('.close .words > p')).toHaveText('A framed e-paper display with no subscription.'); await expect(page.locator('body')).not.toContainText('Reserve'); - await expect(page.locator('.cover .copy > p')).toHaveText('Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.'); - await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display, no subscription.'); - await expect(page.locator('meta[property="og:description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.'); + await expect(page.locator('.cover .copy > p')).toHaveText('A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s.'); + await expect(page.locator('.cover .facts li')).toHaveText(['Hand-assembled in New England.', 'Sets up from your phone. No extra hardware needed.', 'Works almost anywhere: 7,000+ public bird stations worldwide.', 'From $349. No subscription, no camera, no surprises.']); + await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.'); + await expect(page.locator('meta[property="og:description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.'); // no exploded drawing anywhere: the reservation is its headline, the frame on its kickstand, its line and button await expect(page.locator('.exploded, img[src*="exploded"]')).toHaveCount(0); await expect(page.locator('.close > *')).toHaveCount(2); @@ -185,7 +188,7 @@ test('every section and its key copy is there', async ({ page }) => { await expect(page.locator(`${sel} .field .btn`)).toHaveText('Join the waitlist'); await expect(page.locator(`${sel} .field input`)).toHaveAttribute('placeholder', 'you@example.com'); // the note under the field says the one email, upright where the status that replaces it is italic (W-1021) - await expect(page.locator(`${sel} .wl-note`)).toHaveText('One email when Featherframe is available, and nothing else.'); + await expect(page.locator(`${sel} .wl-note`)).toHaveText(sel === '.cover' ? 'The first batch is just 50 frames. The waitlist hears first. No spam.' : 'One email when Featherframe is available, and nothing else.'); await expect(page.locator(`${sel} .wl-note`)).toBeVisible(); await expect(page.locator(`${sel} .wl-note`)).toHaveCSS('font-style', 'normal'); const [field, note] = [await page.locator(`${sel} .field`).boundingBox(), await page.locator(`${sel} .wl-note`).boundingBox()]; @@ -373,7 +376,7 @@ test('the waitlist signs up, then asks which size', async ({ page }) => { await shopRoute(page, 'waitlist', { ok: true, details: 'tok-1', state: 'interest', count: 3 }, signups); await shopRoute(page, 'waitlist/details', { ok: true }, answers); await page.goto('/'); - await expect(page.locator('.cover .wl-note')).toHaveText('One email when Featherframe is available, and nothing else.'); + await expect(page.locator('.cover .wl-note')).toHaveText('The first batch is just 50 frames. The waitlist hears first. No spam.'); await page.fill('#wl-hero-email', 'ada@example.com'); await page.click('.cover .wl button[type=submit]'); await expect.poll(() => signups.length).toBe(1);