From 80a25740e1ed8f18b17a75663b86625e36c06d9d Mon Sep 17 00:00:00 2001
From: Marcus Gibson Pro
"));
+ assert.ok(card, `index.html in the ${build} build has no Pro plan card`);
+ const text = textOf(card.inner);
+ const price = text.indexOf("$19 per user per month, expected");
+ const when = text.indexOf("No launch date yet.");
+ assert.ok(price !== -1, `the Pro card in the ${build} build lost its price`);
+ assert.ok(when > price, `the Pro card in the ${build} build does not say No launch date yet. after the price`);
+ assert.ok(
+ text.includes("Sends the bridge totals, never your file, to Anthropic's Claude. See the privacy page."),
+ `the Pro card in the ${build} build reads "${text}": add the AI commentary note under its item`,
+ );
+ const privacyLinks = [...card.inner.matchAll(/]*\shref="\/privacy\.html#([^"]+)"/gi)];
+ assert.equal(privacyLinks.length, 1, `the Pro card in the ${build} build has ${privacyLinks.length} privacy page links: keep one`);
+ const privacy = applyFeatureBlocks(readPage("privacy.html"), flags);
+ assert.ok(
+ privacy.includes(`id="${privacyLinks[0][1]}"`),
+ `the Pro card in the ${build} build links to /privacy.html#${privacyLinks[0][1]}, which that build of privacy.html does not have`,
+ );
+ }
+});
+
+test("the guide shows the column table right after it says these columns:", () => {
+ const html = readPage("guide.html");
+ const colon = html.indexOf("these columns:
Numbers written like 12,50", colon);
+ assert.ok(numbers > table, "guide.html puts the numbers paragraph before the column table: move it after the table");
+});
+
+test("the guide says when to keep an attribute that describes the customer", () => {
+ const guide = visibleWordsOf(readPage("guide.html")).join(" ");
+ for (const sentence of [
+ "Unless it describes the customer, not the product: that one goes last or can be left out.",
+ "leaving it out raises the match rate",
+ ]) {
+ assert.ok(guide.includes(sentence), `guide.html does not say "${sentence}"`);
+ }
+ assert.ok(
+ !guide.includes("If it barely moves price and raises the match rate"),
+ "guide.html still says the attribute itself raises the match rate: say leaving it out raises it",
+ );
+});
From f89e2c48d02b9e75a4b6c4f919192948d53cfe2e Mon Sep 17 00:00:00 2001
From: Marcus Gibson Use one row per sales line, or per group of lines. A summarized export works as well as invoice detail, as long as it has these columns: Numbers written like 12,50 or 1.234,56 are read with the comma as the decimal mark when every value in the column is written that way. Whole numbers written like 9.600 or 1.234.567 are read with the dot between thousands when the file is separated by semicolons or a value has more than one dot. The Data check tab says when either happened. Numbers written like 12,50 or 1.234,56 are read with the comma as the decimal mark when every value in the column is written that way. Whole numbers written like 9.600 or 1.234.567 are read with the dot between thousands when the file is separated by semicolons or a value has more than one dot. The Data check tab says when either happened. CSV and Excel (.xlsx, first sheet) files are accepted. The file is read in your browser and is not uploaded. To see the layout, download a small sample file of made-up sales. Setup check reruns the bridge with each attribute left out. Price-volume-mix (PVM) analysis Volume, price, and a mix step for every product attribute, in one bridge. A bridge is a chart that starts at last period's sales, adds or subtracts each cause, and ends at this period's sales. Free. No account. CSV or Excel.Run your first bridge
1. Check your file
@@ -52,6 +51,7 @@ Column Needed Notes 1. Check your file
2. Load and set up
@@ -93,8 +93,8 @@ 7. Test your choice of attributes

-
8. Look across more than two periods
diff --git a/index.html b/index.html
index 83b7cce..4cf6648 100644
--- a/index.html
+++ b/index.html
@@ -40,12 +40,13 @@
See why sales moved.
Your file stays in your browser.
For the analyst who has to explain a change in sales or margin. Load a CSV or Excel export, rank your attributes, and read the bridge.
No launch date yet.
Open your browser’s developer tools, go to the network panel, and load a file. No request carries your data. The pages also carry a content security policy that allows connections only to this site, so the browser itself blocks anything else. The checkout page allows connections to Paddle, which takes the payment. The source code is published so anyone can confirm what the page does, and its automated test checks that none of your data is stored or sent.
-Planned Pro features would use a server: saved workspaces would store your setup there, and AI commentary would send the bridge totals, never your file or its rows, to Anthropic’s Claude to write the text. This page will be updated before either is released, and uploading data will never be required to use the free tool.
diff --git a/src/site/home.css b/src/site/home.css index 9b24c7f..6559d4f 100644 --- a/src/site/home.css +++ b/src/site/home.css @@ -93,6 +93,8 @@ a.button.ghost:hover { border-color: var(--plum); background: var(--blush); colo .hero .copy .lead { margin-top: 1.5rem; font-size: 1.3125rem; line-height: 1.45; color: var(--plum-2); max-width: 21rem; } +.hero .copy .define { margin-top: .75rem; max-width: 21rem; font: 400 .9375rem/1.5 var(--sans); color: var(--plum-2); } + .hero .copy .cta { margin-top: 2.25rem; padding: .95rem 1.35rem; font-size: .9375rem; } .hero .copy .cta span { transition: transform .15s; } @@ -104,11 +106,11 @@ a.button.ghost:hover { border-color: var(--plum); background: var(--blush); colo /* In the side-by-side layout, the picture runs into the shell's right padding to leave room for all five KPI tiles. */ .hero figure { margin-right: calc(-1 * var(--pad) - min(0px, var(--gutter) - 2rem)); } -/* The framed picture is a drawing of the tool, so nothing in it answers a pointer, a tap or a long press. */ -.hero figure { pointer-events: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; } +/* The picture links to the sample but still reads as a drawing: no link colour or underline, and no text selection. */ +.hero .frame { display: block; background: var(--t-bg); border-radius: 12px; overflow: hidden; font-family: var(--sans); color: var(--t-text); text-decoration: none; user-select: none; -webkit-user-select: none; + box-shadow: 0 0 0 1px rgba(66, 32, 61, .10), 0 2px 4px rgba(66, 32, 61, .06), 0 24px 60px -12px rgba(66, 32, 61, .35); transition: transform .2s ease, box-shadow .2s ease; } -.hero .frame { background: var(--t-bg); border-radius: 12px; overflow: hidden; font-family: var(--sans); color: var(--t-text); - box-shadow: 0 0 0 1px rgba(66, 32, 61, .10), 0 2px 4px rgba(66, 32, 61, .06), 0 24px 60px -12px rgba(66, 32, 61, .35); } +.hero a.frame:focus-visible { outline: 3px solid var(--plum); outline-offset: 4px; } .hero .chrome { display: flex; align-items: center; gap: .9rem; height: 44px; padding: 0 1rem; border-bottom: 1px solid var(--t-line); } @@ -168,6 +170,20 @@ a.button.ghost:hover { border-color: var(--plum); background: var(--blush); colo .hero .frame { max-width: 48rem; transform: rotate(-1.5deg); } } +@media (hover: hover) { + .hero a.frame:hover { transform: translateY(-4px); box-shadow: 0 0 0 1px rgba(66, 32, 61, .16), 0 4px 8px rgba(66, 32, 61, .08), 0 32px 72px -12px rgba(66, 32, 61, .45); } + + .hero-grid:has(a.frame:hover) .cta span { transform: translateX(3px); } +} + +@media (max-width: 1239px) and (hover: hover) { + .hero a.frame:hover { transform: rotate(-1.5deg) translateY(-4px); } +} + +@media (prefers-reduced-motion: reduce) { + .hero .frame, .hero .copy .cta span { transition: none; } +} + @media (max-width: 800px) { .hero .kpis { grid-template-columns: 1fr 1fr; } @@ -279,10 +295,14 @@ a.button.ghost:hover { border-color: var(--plum); background: var(--blush); colo .plan p { margin-top: 0.5rem; } +.plan .when { margin-top: 0.25rem; font: 400 0.8125rem/1.5 "Instrument Sans", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; color: var(--plum-2); } + .plan ul { margin: 1.25rem 0 1.75rem; padding-left: 1.1rem; display: grid; gap: 0.4rem; } .plan li { font-size: 1.0625rem; line-height: 1.45; } +.plan li small { display: block; margin-top: 0.15rem; font: 400 0.8125rem/1.5 "Instrument Sans", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; color: var(--plum-2); } + /* Footer */ footer { padding-block: 2rem; border-top: 1px solid var(--blush); } @@ -324,6 +344,14 @@ footer { padding-block: 2rem; border-top: 1px solid var(--blush); } .nav nav { width: 100%; justify-content: space-between; gap: 0.5rem; } } +@media (pointer: coarse), (max-width: 480px) { + .wordmark, .nav nav a, .footer-inner a { min-height: 44px; } + + .nav nav a:not(.button), .footer-inner a { display: inline-flex; align-items: center; } + + .footer-inner { row-gap: 0; } +} + @media (max-width: 34rem) { .get dl { grid-template-columns: 1fr; gap: 0.15rem; } From 59db796e083a6ea8c6c47ab22d1f9408ae8c2c12 Mon Sep 17 00:00:00 2001 From: Marcus Gibson