diff --git a/changes/performance%2Fd127.md b/changes/performance%2Fd127.md new file mode 100644 index 0000000..acadcc6 --- /dev/null +++ b/changes/performance%2Fd127.md @@ -0,0 +1,5 @@ +- The picture of the tool on the home page is now a link to the sample data, named "Try it with sample data" like the button beside it. It lifts slightly on hover where a mouse is used, shows a pointer cursor, and has a focus outline. Under reduced motion it does not animate. +- The Pro column says "No launch date yet." after its price. The AI commentary item now says "Sends the bridge totals, never your file, to Anthropic's Claude. See the privacy page." The link goes to the privacy page's AI commentary section when the build has AI commentary, and to its "If this changes" section when it does not. `privacy.html` gains the two section ids `ai-commentary` and `if-this-changes`. +- On phones (coarse pointer or width up to 480 px), every header and footer link on every site page is at least 44 px tall. The desktop look does not change. +- The guide's section 1 shows the column table right after "these columns:", followed by the paragraph on number formats. Section 7 now says "If leaving an attribute out moves price a lot, that attribute carries real mix. Keep it. Unless it describes the customer, not the product: that one goes last or can be left out." and "If it barely moves price and leaving it out raises the match rate, the attribute is adding noise. Consider dropping it." +- The home page explains its first "bridge" with "A bridge is a chart that starts at last period's sales, adds or subtracts each cause, and ends at this period's sales." The "What you get" list now reads "The bridge chart, a mix line for each attribute, a written summary, and an Excel export." where it said "The waterfall". diff --git a/guide.html b/guide.html index c93eff4..91e98b0 100644 --- a/guide.html +++ b/guide.html @@ -37,7 +37,6 @@
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.
| Column | Needed | Notes |
|---|
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.
Try it with sample dataFree. No account. CSV or Excel.
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; } diff --git a/test/site-pages.test.ts b/test/site-pages.test.ts index 75062ca..7ae3ea2 100644 --- a/test/site-pages.test.ts +++ b/test/site-pages.test.ts @@ -115,7 +115,7 @@ function plannedFeaturesOf(html: string, label: string): string[] { ); } return [...list[1].matchAll(/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", + ); +});