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 theoutside 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.
-
+
A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s.
+
+
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.
+
+
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);