carlos / carloku Public

Align pricing comparison sections and disclose longer details

Paul Campbell pushed by claude agent, operated by paul@keymail.dev cdfebfe1d4b5cf7717bb18361d4136d1fae60333 parent bf2ee67
5 files changed, +152 −75
  • copy-review/pricing/build.mjs +27 −7
  • copy-review/pricing/strings.json +3 −3
  • src/pricing/index.html +72 −60
  • src/site.css +46 −1
  • test/pricing-pricebook.test.mjs +4 −4
diff --git a/copy-review/pricing/build.mjs b/copy-review/pricing/build.mjs
index 0feeebb..172798e 100644
--- a/copy-review/pricing/build.mjs
+++ b/copy-review/pricing/build.mjs
@@ -181,16 +181,35 @@ const priceLine = (p) => {
if (p === "free") return priced(id, (cur) => money(cur, unitMinor(p, cur, "A")));
return banded(id, (band, cur) => swapAmount(S[id], cur, money(cur, unitMinor(p, cur, band))));
};
+const infoRow = (label, detail, id) => `<li class="metric-row"><span>${esc(label)}</span><details class="plan-info"><summary aria-label="${esc(label)}" title="${esc(detail)}">i</summary><span class="info-popup">${id ? t(id) : esc(detail)}</span></details></li>`;
const card = (p, featured) => {
const lists = planCardLists[p];
const items = (ids) => ids.map((i) => allowanceLi(p, i)).join("\n");
- const seen = new Set([...(lists.plain || []), ...(lists.allowances || []), ...(lists.features || []), ...(lists.durability || [])].map((i) => `plan.${p}.${i}`));
- for (const k of Object.keys(S).filter((k) => k.startsWith(`plan.${p}.i`))) if (!seen.has(k)) throw new Error("unplaced string " + k);
- return ` <article class="plan${featured ? " featured" : ""}">
- ${t(`plan.${p}.name`, "h3")}
- <p class="price">${priceLine(p)}</p>
-${S[`plan.${p}.min`] ? ` <p class="min">${t(`plan.${p}.min`)}</p>\n` : ""} <p class="for">${t(`plan.${p}.for`)}</p>
-${lists.plain ? ` <ul>\n${items(lists.plain)}\n </ul>\n` : ""}${lists.allowances ? ` <p class="allowance-heading" data-copy="plan.team.allowances">${esc(S["plan.team.allowances"])}</p>\n <ul class="allowances">\n${items(lists.allowances)}\n </ul>\n` : ""}${` <h4 class="allowance-heading">Hibernation &amp; Replication</h4>\n <ul class="durability">\n${p === "free" ? "" : ` <li data-idle-minutes="15">${esc(S["plan.free.i6"].replace("2 minutes", "15 minutes"))}</li>\n`}${items(lists.durability || [])}\n </ul>\n`}${lists.features ? ` <ul class="plan-features">\n${items(lists.features)}\n </ul>\n` : ""} <a class="btn ${featured ? "btn-primary" : "btn-quiet"}" href="https://console.carloku.com/?plan=${p}&amp;currency=&amp;country=" data-pricing-signup data-copy="plan.${p}.cta">${esc(S[`plan.${p}.cta`])}</a>
+ const idle = p === "free" ? 2 : 15;
+ const sleepDetail = S["plan.free.i6"].replace("2 minutes", `${idle} minutes`);
+ const durability = [infoRow(`Apps sleep after ${idle} min`, sleepDetail, p === "free" ? "plan.free.i6" : null)];
+ if (p === "free") {
+ durability.push(infoRow("Data stored on sleep", S["plan.free.i7"], "plan.free.i7"));
+ durability.push(infoRow("Auto-archive", S["plan.free.i8"], "plan.free.i8"));
+ }
+ if (p === "business") durability.push(infoRow("Continuous replication", S["plan.business.i5"], "plan.business.i5"));
+ return ` <article class="plan${featured ? " featured" : ""} plan-${p}">
+ <div class="plan-intro">
+ ${t(`plan.${p}.name`, "h3")}
+ <p class="price">${priceLine(p)}</p>
+ <p class="min">${S[`plan.${p}.min`] ? t(`plan.${p}.min`) : "&nbsp;"}</p>
+ <p class="for">${t(`plan.${p}.for`)}</p>
+ </div>
+ <div class="plan-inclusions">
+ <p class="allowance-heading"${lists.allowances ? ' data-copy="plan.team.allowances"' : ' aria-hidden="true"'}>${lists.allowances ? esc(S["plan.team.allowances"]) : "&nbsp;"}</p>
+ <ul class="allowances">${items(lists.plain || lists.allowances || [])}</ul>
+ </div>
+ <div class="plan-durability">
+ <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
+ <ul class="durability">${durability.join("\n")}</ul>
+ </div>
+ <div class="plan-extras">${lists.features ? `<ul class="plan-features">${items(lists.features)}</ul>` : ""}</div>
+ <a class="btn ${featured ? "btn-primary" : "btn-quiet"}" href="https://console.carloku.com/?plan=${p}&amp;currency=&amp;country=" data-pricing-signup data-copy="plan.${p}.cta">${esc(S[`plan.${p}.cta`])}</a>
</article>`;
};
@@ -272,6 +291,7 @@ ${controls}
<div class="wrap">
${t("plans.heading", "h2")}
<div class="plans">
+ <h3 class="team-plans-heading">Team plans</h3>
${card("free", false)}
${card("personal", true)}
${card("community", false)}
diff --git a/copy-review/pricing/strings.json b/copy-review/pricing/strings.json
index e5c9693..dcae1dd 100644
--- a/copy-review/pricing/strings.json
+++ b/copy-review/pricing/strings.json
@@ -199,7 +199,7 @@
"id": "plan.community.price",
"section": "The plans",
"label": "Community price",
- "text": "€5/y per member",
+ "text": "€5/y/member",
"context": "The price line on the Community card."
},
{
@@ -262,7 +262,7 @@
"id": "plan.business.price",
"section": "The plans",
"label": "Business price",
- "text": "€50/y per member",
+ "text": "€50/y/member",
"context": "The price line on the Business card."
},
{
@@ -304,7 +304,7 @@
"id": "plan.business.i4",
"section": "The plans",
"label": "Business inclusion 4",
- "text": "30,000 replication writes a month",
+ "text": "30,000 replication writes/m",
"context": "An allowance in the team plan card, under the shared heading “Each member adds”."
},
{
diff --git a/src/pricing/index.html b/src/pricing/index.html
index a292c43..62632f7 100644
--- a/src/pricing/index.html
+++ b/src/pricing/index.html
@@ -63,80 +63,92 @@
<div class="wrap">
<h2>The plans</h2>
<div class="plans">
- <article class="plan">
- <h3>Free</h3>
- <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$0&quot;,&quot;EUR&quot;:&quot;€0&quot;,&quot;GBP&quot;:&quot;£0&quot;}">$0</span></p>
- <p class="for"><span>Free to try, great for tiny apps.</span></p>
- <ul>
- <li>One member</li>
+ <h3 class="team-plans-heading">Team plans</h3>
+ <article class="plan plan-free">
+ <div class="plan-intro">
+ <h3>Free</h3>
+ <p class="price"><span data-currency-values="{&quot;USD&quot;:&quot;$0&quot;,&quot;EUR&quot;:&quot;€0&quot;,&quot;GBP&quot;:&quot;£0&quot;}">$0</span></p>
+ <p class="min">&nbsp;</p>
+ <p class="for"><span>Free to try, great for tiny apps.</span></p>
+ </div>
+ <div class="plan-inclusions">
+ <p class="allowance-heading" aria-hidden="true">&nbsp;</p>
+ <ul class="allowances"> <li>One member</li>
<li>3 apps</li>
<li>100 awake hours a month</li>
<li>250 MB of storage</li>
- <li>1 GB of transfer a month</li>
- </ul>
- <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
- <ul class="durability">
- <li>Apps sleep after 2 minutes idle</li>
- <li>Data stored on sleep. No data replication, no data restore.</li>
- <li>Apps not visited after 90 days are archived with notice. These apps can be restored later by request.</li>
- </ul>
+ <li>1 GB of transfer a month</li></ul>
+ </div>
+ <div class="plan-durability">
+ <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
+ <ul class="durability"><li class="metric-row"><span>Apps sleep after 2 min</span><details class="plan-info"><summary aria-label="Apps sleep after 2 min" title="Apps sleep after 2 minutes idle">i</summary><span class="info-popup"><span>Apps sleep after 2 minutes idle</span></span></details></li>
+<li class="metric-row"><span>Data stored on sleep</span><details class="plan-info"><summary aria-label="Data stored on sleep" title="Data stored on sleep. No data replication, no data restore.">i</summary><span class="info-popup"><span>Data stored on sleep. No data replication, no data restore.</span></span></details></li>
+<li class="metric-row"><span>Auto-archive</span><details class="plan-info"><summary aria-label="Auto-archive" title="Apps not visited after 90 days are archived with notice. These apps can be restored later by request.">i</summary><span class="info-popup"><span>Apps not visited after 90 days are archived with notice. These apps can be restored later by request.</span></span></details></li></ul>
+ </div>
+ <div class="plan-extras"></div>
<a class="btn btn-quiet" href="https://console.carloku.com/?plan=free&amp;currency=&amp;country=" data-pricing-signup>Create an account</a>
</article>
- <article class="plan featured">
- <h3>Personal</h3>
- <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$28/y&quot;,&quot;EUR&quot;:&quot;€24/y&quot;,&quot;GBP&quot;:&quot;£22/y&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$21/y&quot;,&quot;EUR&quot;:&quot;€18/y&quot;,&quot;GBP&quot;:&quot;£16.50/y&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$14/y&quot;,&quot;EUR&quot;:&quot;€12/y&quot;,&quot;GBP&quot;:&quot;£11/y&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$9.80/y&quot;,&quot;EUR&quot;:&quot;€8.40/y&quot;,&quot;GBP&quot;:&quot;£7.70/y&quot;}}">$28/y</span></p>
- <p class="for"><span>For individuals running a few apps.</span></p>
- <ul>
- <li>One member</li>
+ <article class="plan featured plan-personal">
+ <div class="plan-intro">
+ <h3>Personal</h3>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$28/y&quot;,&quot;EUR&quot;:&quot;€24/y&quot;,&quot;GBP&quot;:&quot;£22/y&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$21/y&quot;,&quot;EUR&quot;:&quot;€18/y&quot;,&quot;GBP&quot;:&quot;£16.50/y&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$14/y&quot;,&quot;EUR&quot;:&quot;€12/y&quot;,&quot;GBP&quot;:&quot;£11/y&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$9.80/y&quot;,&quot;EUR&quot;:&quot;€8.40/y&quot;,&quot;GBP&quot;:&quot;£7.70/y&quot;}}">$28/y</span></p>
+ <p class="min">&nbsp;</p>
+ <p class="for"><span>For individuals running a few apps.</span></p>
+ </div>
+ <div class="plan-inclusions">
+ <p class="allowance-heading" aria-hidden="true">&nbsp;</p>
+ <ul class="allowances"> <li>One member</li>
<li data-band-values="{&quot;A&quot;:&quot;85 awake hours a month&quot;,&quot;B&quot;:&quot;63.75 awake hours a month&quot;,&quot;C&quot;:&quot;42.5 awake hours a month&quot;,&quot;D&quot;:&quot;29.75 awake hours a month&quot;}">85 awake hours a month</li>
<li data-band-values="{&quot;A&quot;:&quot;2 GB of storage&quot;,&quot;B&quot;:&quot;1.5 GB of storage&quot;,&quot;C&quot;:&quot;1 GB of storage&quot;,&quot;D&quot;:&quot;0.7 GB of storage&quot;}">2 GB of storage</li>
- <li data-band-values="{&quot;A&quot;:&quot;2 GB of transfer a month&quot;,&quot;B&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;C&quot;:&quot;1 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.7 GB of transfer a month&quot;}">2 GB of transfer a month</li>
- </ul>
- <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
- <ul class="durability">
- <li data-idle-minutes="15">Apps sleep after 15 minutes idle</li>
-
- </ul>
+ <li data-band-values="{&quot;A&quot;:&quot;2 GB of transfer a month&quot;,&quot;B&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;C&quot;:&quot;1 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.7 GB of transfer a month&quot;}">2 GB of transfer a month</li></ul>
+ </div>
+ <div class="plan-durability">
+ <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
+ <ul class="durability"><li class="metric-row"><span>Apps sleep after 15 min</span><details class="plan-info"><summary aria-label="Apps sleep after 15 min" title="Apps sleep after 15 minutes idle">i</summary><span class="info-popup">Apps sleep after 15 minutes idle</span></details></li></ul>
+ </div>
+ <div class="plan-extras"></div>
<a class="btn btn-primary" href="https://console.carloku.com/?plan=personal&amp;currency=&amp;country=" data-pricing-signup>Choose Personal</a>
</article>
- <article class="plan">
- <h3>Community</h3>
- <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$6/y per member&quot;,&quot;EUR&quot;:&quot;€5/y per member&quot;,&quot;GBP&quot;:&quot;£4.50/y per member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$4.50/y per member&quot;,&quot;EUR&quot;:&quot;€3.75/y per member&quot;,&quot;GBP&quot;:&quot;£3.38/y per member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$3/y per member&quot;,&quot;EUR&quot;:&quot;€2.50/y per member&quot;,&quot;GBP&quot;:&quot;£2.25/y per member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$2.10/y per member&quot;,&quot;EUR&quot;:&quot;€1.75/y per member&quot;,&quot;GBP&quot;:&quot;£1.58/y per member&quot;}}">$6/y per member</span></p>
- <p class="min"><span>Minimum 10 members.</span></p>
- <p class="for"><span>For a groups, clubs, classes, an open-source projects.</span></p>
- <p class="allowance-heading">Each member adds</p>
- <ul class="allowances">
- <li data-band-values="{&quot;A&quot;:&quot;17 awake hours a month&quot;,&quot;B&quot;:&quot;12.75 awake hours a month&quot;,&quot;C&quot;:&quot;8.5 awake hours a month&quot;,&quot;D&quot;:&quot;5.95 awake hours a month&quot;}">17 awake hours a month</li>
+ <article class="plan plan-community">
+ <div class="plan-intro">
+ <h3>Community</h3>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$6/y/member&quot;,&quot;EUR&quot;:&quot;€5/y/member&quot;,&quot;GBP&quot;:&quot;£4.50/y/member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$4.50/y/member&quot;,&quot;EUR&quot;:&quot;€3.75/y/member&quot;,&quot;GBP&quot;:&quot;£3.38/y/member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$3/y/member&quot;,&quot;EUR&quot;:&quot;€2.50/y/member&quot;,&quot;GBP&quot;:&quot;£2.25/y/member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$2.10/y/member&quot;,&quot;EUR&quot;:&quot;€1.75/y/member&quot;,&quot;GBP&quot;:&quot;£1.58/y/member&quot;}}">$6/y/member</span></p>
+ <p class="min"><span>Minimum 10 members.</span></p>
+ <p class="for"><span>For a groups, clubs, classes, an open-source projects.</span></p>
+ </div>
+ <div class="plan-inclusions">
+ <p class="allowance-heading">Each member adds</p>
+ <ul class="allowances"> <li data-band-values="{&quot;A&quot;:&quot;17 awake hours a month&quot;,&quot;B&quot;:&quot;12.75 awake hours a month&quot;,&quot;C&quot;:&quot;8.5 awake hours a month&quot;,&quot;D&quot;:&quot;5.95 awake hours a month&quot;}">17 awake hours a month</li>
<li data-band-values="{&quot;A&quot;:&quot;0.5 GB of storage&quot;,&quot;B&quot;:&quot;0.375 GB of storage&quot;,&quot;C&quot;:&quot;0.25 GB of storage&quot;,&quot;D&quot;:&quot;0.175 GB of storage&quot;}">0.5 GB of storage</li>
- <li data-band-values="{&quot;A&quot;:&quot;0.4 GB of transfer a month&quot;,&quot;B&quot;:&quot;0.3 GB of transfer a month&quot;,&quot;C&quot;:&quot;0.2 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.14 GB of transfer a month&quot;}">0.4 GB of transfer a month</li>
- </ul>
- <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
- <ul class="durability">
- <li data-idle-minutes="15">Apps sleep after 15 minutes idle</li>
-
- </ul>
+ <li data-band-values="{&quot;A&quot;:&quot;0.4 GB of transfer a month&quot;,&quot;B&quot;:&quot;0.3 GB of transfer a month&quot;,&quot;C&quot;:&quot;0.2 GB of transfer a month&quot;,&quot;D&quot;:&quot;0.14 GB of transfer a month&quot;}">0.4 GB of transfer a month</li></ul>
+ </div>
+ <div class="plan-durability">
+ <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
+ <ul class="durability"><li class="metric-row"><span>Apps sleep after 15 min</span><details class="plan-info"><summary aria-label="Apps sleep after 15 min" title="Apps sleep after 15 minutes idle">i</summary><span class="info-popup">Apps sleep after 15 minutes idle</span></details></li></ul>
+ </div>
+ <div class="plan-extras"></div>
<a class="btn btn-quiet" href="https://console.carloku.com/?plan=community&amp;currency=&amp;country=" data-pricing-signup>Choose Community</a>
</article>
- <article class="plan">
- <h3>Business</h3>
- <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$60/y per member&quot;,&quot;EUR&quot;:&quot;€50/y per member&quot;,&quot;GBP&quot;:&quot;£45/y per member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$45/y per member&quot;,&quot;EUR&quot;:&quot;€37.50/y per member&quot;,&quot;GBP&quot;:&quot;£33.75/y per member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$30/y per member&quot;,&quot;EUR&quot;:&quot;€25/y per member&quot;,&quot;GBP&quot;:&quot;£22.50/y per member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$21/y per member&quot;,&quot;EUR&quot;:&quot;€17.50/y per member&quot;,&quot;GBP&quot;:&quot;£15.75/y per member&quot;}}">$60/y per member</span></p>
- <p class="min"><span>Minimum 10 members.</span></p>
- <p class="for"><span>For teams that need continuous replication and restore.</span></p>
- <p class="allowance-heading">Each member adds</p>
- <ul class="allowances">
- <li data-band-values="{&quot;A&quot;:&quot;170 extra awake hours/m&quot;,&quot;B&quot;:&quot;127.5 extra awake hours/m&quot;,&quot;C&quot;:&quot;85 extra awake hours/m&quot;,&quot;D&quot;:&quot;59.5 extra awake hours/m&quot;}">170 extra awake hours/m</li>
+ <article class="plan plan-business">
+ <div class="plan-intro">
+ <h3>Business</h3>
+ <p class="price"><span data-band-values="{&quot;A&quot;:{&quot;USD&quot;:&quot;$60/y/member&quot;,&quot;EUR&quot;:&quot;€50/y/member&quot;,&quot;GBP&quot;:&quot;£45/y/member&quot;},&quot;B&quot;:{&quot;USD&quot;:&quot;$45/y/member&quot;,&quot;EUR&quot;:&quot;€37.50/y/member&quot;,&quot;GBP&quot;:&quot;£33.75/y/member&quot;},&quot;C&quot;:{&quot;USD&quot;:&quot;$30/y/member&quot;,&quot;EUR&quot;:&quot;€25/y/member&quot;,&quot;GBP&quot;:&quot;£22.50/y/member&quot;},&quot;D&quot;:{&quot;USD&quot;:&quot;$21/y/member&quot;,&quot;EUR&quot;:&quot;€17.50/y/member&quot;,&quot;GBP&quot;:&quot;£15.75/y/member&quot;}}">$60/y/member</span></p>
+ <p class="min"><span>Minimum 10 members.</span></p>
+ <p class="for"><span>For teams that need continuous replication and restore.</span></p>
+ </div>
+ <div class="plan-inclusions">
+ <p class="allowance-heading">Each member adds</p>
+ <ul class="allowances"> <li data-band-values="{&quot;A&quot;:&quot;170 extra awake hours/m&quot;,&quot;B&quot;:&quot;127.5 extra awake hours/m&quot;,&quot;C&quot;:&quot;85 extra awake hours/m&quot;,&quot;D&quot;:&quot;59.5 extra awake hours/m&quot;}">170 extra awake hours/m</li>
<li data-band-values="{&quot;A&quot;:&quot;5 GB/m extra of storage&quot;,&quot;B&quot;:&quot;3.75 GB/m extra of storage&quot;,&quot;C&quot;:&quot;2.5 GB/m extra of storage&quot;,&quot;D&quot;:&quot;1.75 GB/m extra of storage&quot;}">5 GB/m extra of storage</li>
<li data-band-values="{&quot;A&quot;:&quot;3 GB of transfer a month&quot;,&quot;B&quot;:&quot;2.25 GB of transfer a month&quot;,&quot;C&quot;:&quot;1.5 GB of transfer a month&quot;,&quot;D&quot;:&quot;1.05 GB of transfer a month&quot;}">3 GB of transfer a month</li>
- <li data-band-values="{&quot;A&quot;:&quot;30,000 replication writes a month&quot;,&quot;B&quot;:&quot;22,500 replication writes a month&quot;,&quot;C&quot;:&quot;15,000 replication writes a month&quot;,&quot;D&quot;:&quot;10,500 replication writes a month&quot;}">30,000 replication writes a month</li>
- </ul>
- <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
- <ul class="durability">
- <li data-idle-minutes="15">Apps sleep after 15 minutes idle</li>
- <li>Continuous replication, with restore to an earlier moment</li>
- </ul>
- <ul class="plan-features">
- <li>Priority support</li>
- </ul>
+ <li data-band-values="{&quot;A&quot;:&quot;30,000 replication writes/m&quot;,&quot;B&quot;:&quot;22,500 replication writes/m&quot;,&quot;C&quot;:&quot;15,000 replication writes/m&quot;,&quot;D&quot;:&quot;10,500 replication writes/m&quot;}">30,000 replication writes/m</li></ul>
+ </div>
+ <div class="plan-durability">
+ <h4 class="allowance-heading">Hibernation &amp; Replication</h4>
+ <ul class="durability"><li class="metric-row"><span>Apps sleep after 15 min</span><details class="plan-info"><summary aria-label="Apps sleep after 15 min" title="Apps sleep after 15 minutes idle">i</summary><span class="info-popup">Apps sleep after 15 minutes idle</span></details></li>
+<li class="metric-row"><span>Continuous replication</span><details class="plan-info"><summary aria-label="Continuous replication" title="Continuous replication, with restore to an earlier moment">i</summary><span class="info-popup"><span>Continuous replication, with restore to an earlier moment</span></span></details></li></ul>
+ </div>
+ <div class="plan-extras"><ul class="plan-features"> <li>Priority support</li></ul></div>
<a class="btn btn-quiet" href="https://console.carloku.com/?plan=business&amp;currency=&amp;country=" data-pricing-signup>Choose Business</a>
</article>
</div>
diff --git a/src/site.css b/src/site.css
index 6634d1b..e1b8b2c 100644
--- a/src/site.css
+++ b/src/site.css
@@ -139,7 +139,7 @@ html { scrollbar-color: var(--line-strong) transparent; }
.site-head .head-cta:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
@media (max-width: 44rem) {
.site-head .wrap { flex-wrap: wrap; height: auto; padding-top: 0.7rem; padding-bottom: 0.7rem; row-gap: 0.35rem; }
- .site-head nav { flex-basis: 100%; order: 3; gap: 1.25rem; margin-left: 0; }
+ .site-head nav { flex-wrap: wrap; flex-basis: 100%; order: 3; gap: 1.25rem; margin-left: 0; }
.site-head nav a { font-size: 0.9rem; }
.site-head .head-cta { margin-left: auto; padding: 0.3rem 0.8rem; font-size: 0.9rem; }
}
@@ -455,3 +455,48 @@ article.guide .note {
.location-change { text-decoration: underline; text-underline-offset: 0.2em; }
.pricing-location[open] > summary { margin-bottom: 1.5rem; }
.pricing-location .currency-form { margin-top: 1rem; }
+
+/* Shared rows keep the comparison aligned as descriptions change length. */
+.plans { column-gap: 1.25rem; row-gap: 0; }
+.team-plans-heading { grid-column: 3 / span 2; grid-row: 1; text-align: center; font-size: 0.92rem; font-weight: 620; padding: 0 0 0.75rem; color: var(--muted); }
+.plan { display: grid; grid-template-rows: subgrid; grid-row: 2 / span 5; row-gap: 0; padding-inline: 1.1rem; }
+.plan-free { grid-column: 1; }
+.plan-personal { grid-column: 2; }
+.plan-community { grid-column: 3; }
+.plan-business { grid-column: 4; }
+.plan .price { font-size: clamp(1.1rem, 1.6vw, 1.4rem); white-space: nowrap; }
+.plan .min { min-height: 1.4em; }
+.plan .for { min-height: 0; margin-bottom: 1.25rem; }
+.plan .allowance-heading { margin-top: 0; font-size: 0.82rem; white-space: nowrap; }
+.plan-inclusions { padding-bottom: 1.1rem; }
+.plan .allowances, .plan .durability { margin-bottom: 0; }
+.plan .plan-features { margin: 1rem 0 0; }
+.plan .btn { margin-top: 1.5rem; align-self: end; }
+.plan li { font-size: 0.84rem; }
+.plan .metric-row { display: flex; align-items: center; justify-content: space-between; gap: 0.35rem; padding-left: 0; white-space: nowrap; }
+.plan .metric-row::before { display: none; }
+.plan-info { position: relative; flex-shrink: 0; }
+.plan-info summary { display: flex; align-items: center; justify-content: center; width: 1.15rem; height: 1.15rem; border: 1px solid var(--line-strong); border-radius: 50%; list-style: none; cursor: pointer; font-size: 0.75rem; color: var(--muted); }
+.plan-info summary::-webkit-details-marker { display: none; }
+.plan-info summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
+.info-popup { position: absolute; z-index: 10; right: 0; top: calc(100% + 0.5rem); width: min(18rem, 70vw); padding: 0.8rem; border: 1px solid var(--line-strong); border-radius: 0.6rem; background: var(--bg); box-shadow: var(--shadow); font-size: 0.86rem; line-height: 1.55; white-space: normal; }
+@media (max-width: 72rem) {
+ .plans { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .plan-free, .plan-personal { grid-row: 1 / span 5; }
+ .team-plans-heading { grid-column: 1 / span 2; grid-row: 6; padding-top: 1.75rem; }
+ .plan-community, .plan-business { grid-row: 7 / span 5; }
+ .plan-free, .plan-community { grid-column: 1; }
+ .plan-personal, .plan-business { grid-column: 2; }
+ .plan .price { font-size: 1.4rem; }
+}
+@media (max-width: 44rem) {
+ .plans { grid-template-columns: 1fr; }
+ .price-table { display: block; overflow-x: auto; }
+ .plan { display: flex; grid-column: 1; grid-row: auto; margin-bottom: 1rem; }
+ .team-plans-heading { grid-column: 1; grid-row: 3; }
+ .plan-free { grid-row: 1; } .plan-personal { grid-row: 2; }
+ .plan-community { grid-row: 4; } .plan-business { grid-row: 5; }
+ .plan-inclusions > .allowance-heading[aria-hidden] { display: none; }
+ .plan .allowance-heading { font-size: 0.92rem; }
+ .plan li { font-size: 0.92rem; }
+}
diff --git a/test/pricing-pricebook.test.mjs b/test/pricing-pricebook.test.mjs
index ecacde7..f4a0ac3 100644
--- a/test/pricing-pricebook.test.mjs
+++ b/test/pricing-pricebook.test.mjs
@@ -69,8 +69,8 @@ test('the static page, before any script runs, is the default currency at band A
test('plan prices and the band table are the book, per band and currency', () => {
// Every plan, band and currency slot is exactly its exported amount with
// the plan's suffix (review fourteen: a prefix match let "$30/y per
- // member" pass for "$3/y per member").
- for (const [k, id, suffix] of [['personal', 'plan.personal.price', '/y'], ['community', 'plan.community.price', '/y per member'], ['business', 'plan.business.price', '/y per member']]) {
+ // member" pass for "$3/y/member").
+ for (const [k, id, suffix] of [['personal', 'plan.personal.price', '/y'], ['community', 'plan.community.price', '/y/member'], ['business', 'plan.business.price', '/y/member']]) {
const v = attr(id, 'data-band-values');
for (const band of ['A', 'B', 'C', 'D']) for (const cur of ['USD', 'EUR', 'GBP']) {
assert.equal(v[band][cur], money(sym[cur], planOf(k).annual_unit_minor[cur.toLowerCase()][band]) + suffix, `${k} ${band} ${cur}`);
@@ -79,7 +79,7 @@ test('plan prices and the band table are the book, per band and currency', () =>
// The free plan's price in every currency.
assert.deepEqual(attr('plan.free.price', 'data-currency-values'), Object.fromEntries(['USD', 'EUR', 'GBP'].map((cur) => [cur, money(sym[cur], planOf('free').annual_unit_minor[cur.toLowerCase()].A)])));
assert.equal(attr('plan.personal.price', 'data-band-values').D.GBP, '£7.70/y');
- assert.equal(attr('plan.business.price', 'data-band-values').D.GBP, '£15.75/y per member');
+ assert.equal(attr('plan.business.price', 'data-band-values').D.GBP, '£15.75/y/member');
const cells = valuesOf(page.slice(page.indexOf('class="price-table"'), page.indexOf('class="price-table rates"')));
assert.equal(cells.length, 12);
let i = 0;
@@ -97,7 +97,7 @@ test('a band-scaled inclusion carries each band\'s figure by the platform\'s rul
assert.deepEqual(attr('plan.personal.i2', 'data-band-values'), { A: S['plan.personal.i2'], B: '63.75 awake hours a month', C: '42.5 awake hours a month', D: '29.75 awake hours a month' });
assert.equal(per('personal', 'awake_seconds', 'D') / 3600, 29.75);
assert.deepEqual(attr('plan.community.i2', 'data-band-values'), { A: S['plan.community.i2'], B: '0.375 GB of storage', C: '0.25 GB of storage', D: '0.175 GB of storage' });
- assert.deepEqual(attr('plan.business.i4', 'data-band-values'), { A: S['plan.business.i4'], B: '22,500 replication writes a month', C: '15,000 replication writes a month', D: '10,500 replication writes a month' });
+ assert.deepEqual(attr('plan.business.i4', 'data-band-values'), { A: S['plan.business.i4'], B: '22,500 replication writes/m', C: '15,000 replication writes/m', D: '10,500 replication writes/m' });
assert.equal(per('business', 'writes', 'D'), 10500);
// Strings with no figure, and the free plan's, are not banded.
for (const id of ['plan.personal.i1', 'plan.free.i3', 'plan.business.i5']) assert.ok(!preview.match(new RegExp(`data-copy="${re(id)}" data-band-values`)), id);