carlos / platform Public

Merge commit 'c1ae91a0' into feat/carloku-multi-currency-20261001

claude agent, operated by paul@keymail.dev 5047494b80debfd97f7ecaf480f948f09fbd5b6b parent a9304f1parent c1ae91a
3 files changed, +423 −155
  • implementation/carloku-paid-launch/copy-review/billing-page/preview/index.html +363 −148
  • internal/console/templates/billing.html +25 −7
  • internal/console/templates/layout.html +35 −0
diff --git a/implementation/carloku-paid-launch/copy-review/billing-page/preview/index.html b/implementation/carloku-paid-launch/copy-review/billing-page/preview/index.html
index 412ca779a..007eedd41 100644
--- a/implementation/carloku-paid-launch/copy-review/billing-page/preview/index.html
+++ b/implementation/carloku-paid-launch/copy-review/billing-page/preview/index.html
@@ -40,19 +40,19 @@
overflow-x: auto; font-family: var(--mono); font-size: 0.85rem; line-height: 1.6;
}
pre code { background: none; padding: 0; }
-
+
.changelog { white-space: pre-wrap; }
select, input[type=text], input[type=search] {
font: inherit; font-family: var(--sans); font-size: 0.9rem; color: var(--ink); background: var(--bg);
border: 1px solid var(--rule); border-radius: 0.4rem; padding: 0.3rem 0.5rem;
}
td form { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin: 0; }
-
+
form.inline { display: inline; margin: 0; }
form.inline button { vertical-align: baseline; }
-
+
form.listctl { display: flex; gap: 0.6rem; flex-wrap: wrap; margin: 0 0 0.6rem; }
-
+
.icon { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; }
header {
max-width: 64rem; margin: 0 auto; padding: 1.4rem 0;
@@ -65,7 +65,7 @@
font-size: 0.9rem;
}
header .addr { color: var(--muted); }
-
+
header .switcher {
position: relative;
}
@@ -76,12 +76,12 @@
}
header .switcher > summary::-webkit-details-marker { display: none; }
header .switcher > summary:hover { background: var(--accent-soft); }
-
+
header .switcher .current-name {
font-weight: 700; min-width: 0; max-width: 14rem;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
-
+
header .switcher > summary > .icon { color: var(--muted); align-self: center; width: 0.9em; }
header .switcher[open] > summary > .icon { transform: rotate(180deg); }
header .switcher-menu {
@@ -99,7 +99,7 @@
}
header .switcher-menu a:hover, header .switcher-menu a:focus { background: var(--accent-soft); }
header .switcher-menu .current { font-weight: 600; background: var(--accent-soft); }
-
+
header .switcher-menu .mark {
color: var(--accent); width: 0.9rem; flex: none;
display: flex; align-self: center;
@@ -112,7 +112,7 @@
h1 { font-size: 1.9rem; line-height: 1.15; margin: 0 0 1rem; }
h2 { font-size: 1.25rem; margin: 2rem 0 0.75rem; }
h3 { font-size: 1rem; margin: 1.5rem 0 0.5rem; }
-
+
.settings-row { display: flex; gap: 0.6rem; flex-wrap: wrap; align-items: center; margin: 0 0 0.75rem; }
.settings-layout {
display: grid; grid-template-columns: minmax(10rem, 13rem) minmax(0, 1fr);
@@ -133,7 +133,7 @@
p { max-width: 40rem; margin: 0 0 1rem; }
.lede { font-size: 1.15rem; color: var(--muted); }
.small { font-size: 0.9rem; color: var(--muted); }
-
+
.visually-hidden {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip-path: inset(50%); white-space: nowrap;
@@ -154,7 +154,7 @@
.instance-facts dt { color: var(--muted); }
.instance-facts dd { margin: 0; overflow-wrap: anywhere; }
.instance-facts .cellspark { max-width: 24rem; }
-
+
#logs-filter { display: flex; flex-wrap: wrap; align-items: end; gap: 0.65rem; margin-bottom: 1rem; }
#logs-filter label { display: flex; flex-direction: column; gap: 0.25rem; max-width: 100%; }
#logs-filter select, #logs-filter input { max-width: 100%; margin: 0; }
@@ -165,21 +165,21 @@
min-height: 16rem; max-height: 70vh; overflow: auto;
font-family: var(--mono); font-size: 0.8rem; line-height: 1.55;
}
-
+
.tl { white-space: pre; width: max-content; min-width: 100%; padding-right: 1rem; }
-
+
.tl-t, .tl-h, .tl-f { color: var(--muted); }
.tl-s { color: var(--accent); }
-
+
.tl-warn .tl-m { color: var(--warn, #a6790f); }
.tl-error .tl-m { color: var(--bad); font-weight: 600; }
table { border-collapse: collapse; width: 100%; margin: 0 0 1rem; font-size: 0.95rem; }
.tiles { display: flex; flex-wrap: wrap; gap: 1rem; margin: 0 0 1.5rem; }
.tile { flex: 1 1 10rem; background: var(--accent-soft); border-radius: 0.6rem; padding: 1rem 1.1rem; }
-
+
a.tile { color: var(--ink); text-decoration: none; }
a.tile:hover { outline: 1px solid var(--accent); }
-
+
.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.tile h2 {
margin: 0 0 0.35rem; font-family: var(--mono); font-size: 0.78rem; text-transform: uppercase;
@@ -187,30 +187,30 @@
}
.tile-value { font-size: 1.6rem; font-weight: 600; margin: 0; }
.tile-detail { color: var(--muted); font-size: 0.8rem; margin: 0.25rem 0 0; }
-
+
.chip {
display: inline-block; font-family: var(--mono); font-size: 0.78rem; font-weight: 500;
padding: 0.15rem 0.55rem; border-radius: 999px; margin: 0.15rem 0.3rem 0.15rem 0;
}
.chip-stale { border: 1px solid var(--warn, #a6790f); color: var(--warn, #a6790f); }
-
+
.chip-canary { border: 1px solid var(--accent); color: var(--accent); }
.chip-prod { background: var(--bad); color: var(--bg); }
-
+
.chip-expired { border: 1px solid var(--warn, #a6790f); color: var(--warn, #a6790f); }
-
+
.chip-env { border: 1px solid var(--rule); color: var(--muted); }
-
+
.chip-health-ok { border: 1px solid var(--good); color: var(--good); }
.chip-health-attention { background: var(--warn, #a6790f); color: var(--bg); }
.chip-health-notdeployed { border: 1px solid var(--rule); color: var(--muted); }
.chip-health-unknown { border: 1px solid var(--warn, #a6790f); color: var(--warn, #a6790f); }
-
+
.chip-freetier-inside { border: 1px solid var(--warn, #a6790f); color: var(--warn, #a6790f); }
.chip-freetier-partial { border: 1px solid var(--accent); color: var(--accent); }
.chip-freetier-paying { border: 1px solid var(--good); color: var(--good); }
.chip-freetier-pooled { border: 1px solid var(--rule); color: var(--muted); }
-
+
.chip-tier-unassigned { border: 1px solid var(--rule); color: var(--muted); }
.chip-tier-try { border: 1px solid var(--rule); color: var(--muted); }
.chip-tier-pool { border: 1px solid var(--rule); color: var(--muted); }
@@ -221,15 +221,15 @@
.chip-up { background: var(--accent-soft); color: #2f7a4f; }
.chip-down { background: var(--accent-soft); color: var(--bad); }
.chips { display: flex; flex-wrap: wrap; margin: 0 0 1rem; }
-
+
.bar { display: inline-block; width: 6rem; height: 0.5rem; border-radius: 999px; background: var(--accent-soft); vertical-align: middle; overflow: hidden; }
.bar-fill { display: block; height: 100%; background: var(--accent); }
-
+
.knob-row { display: flex; gap: 0.6rem; align-items: center; margin: 0 0 0.5rem; flex-wrap: wrap; }
.knob-row label { min-width: 12rem; color: var(--muted); font-size: 0.9rem; }
.knob-row input[type=number], .knob-row input[type=text] { width: 8rem; }
.knob-row input[type=range] { width: 12rem; }
-
+
.flow { margin: 0 0 1.75rem; }
.flow-line {
--flow-y: 0.52rem;
@@ -254,7 +254,7 @@
letter-spacing: 0.05em; color: var(--muted); font-weight: 600;
}
.flow-version { margin: 0.3rem 0 0; font-size: 0.95rem; }
-
+
.flow-stage:not(.is-held) .flow-version { color: var(--muted); }
.flow-note { margin: 0.25rem 0 0; font-size: 0.78rem; color: var(--muted); }
.flow-addrs {
@@ -270,7 +270,7 @@
.flow-tone-bad .flow-health { color: var(--bad); font-weight: 600; }
.flow-band { margin: 1.4rem 0 0.7rem; font-size: 0.9rem; color: var(--muted); }
.flow-caption { margin: 1.1rem 0 0; }
-
+
@media (max-width: 46rem) {
.settings-layout { display: block; }
.settings-nav {
@@ -292,7 +292,7 @@
.flow-stage.is-carried::before { border-left-color: var(--accent); }
.flow-stage.is-carried::after { border-top-color: var(--accent); border-left-color: transparent; }
}
-
+
.ladder { list-style: none; margin: 0 0 1rem; padding: 0; max-width: 40rem; }
.rung {
border: 1px solid var(--rule); border-radius: 0.6rem; padding: 0.8rem 1rem;
@@ -307,7 +307,7 @@
.rung .gate { margin: 0.5rem 0 0; }
.rung .routes { list-style: none; margin: 0.3rem 0 0.6rem; padding: 0; font-size: 0.95rem; }
.rung .routes li { padding: 0.1rem 0; }
-
+
.step {
display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
margin: 0.7rem -1rem -0.8rem; padding: 0.5rem 1rem;
@@ -335,62 +335,62 @@
}
button:not(.linkish):hover, .btn:hover { background: var(--accent-strong); }
button.linkish.danger { color: var(--bad); }
-
+
.btn-sm, button.btn-sm {
font-size: 0.85rem; padding: 0.35rem 0.8rem; border-radius: 0.4rem;
}
button.btn-danger, .btn-danger { background: var(--bad); color: var(--bg); }
button.btn-danger:hover, .btn-danger:hover { background: var(--bad); filter: brightness(0.9); }
-
+
td .cell-value { display: inline-block; min-width: 3.5rem; font-weight: 600; }
.cellspark { width: 6rem; height: 1.1rem; vertical-align: -0.2rem; color: var(--muted); }
.cellspark polyline { stroke-width: 2.5; }
-
+
.feed { list-style: none; margin: 0 0 0.5rem; padding: 0; max-width: 40rem; }
.feed li { padding: 0.25rem 0; border-bottom: 1px solid var(--rule); }
.feed li:last-child { border-bottom: 0; }
-
+
.confirm-card {
border: 1px solid var(--rule); border-radius: 0.6rem;
padding: 1.25rem 1.4rem; max-width: 40rem; margin: 0 0 1rem;
}
.confirm-card .actions { display: flex; gap: 0.9rem; align-items: center; margin-top: 1rem; }
.confirm-card .confirm-note { margin: 0.8rem 0 0; }
-
-
+
+
.archwrap { overflow-x: auto; margin: 0.2rem 0 1.6rem; }
-
+
h4.archband {
border-top: 1px solid var(--rule); color: var(--muted);
font: 600 0.8rem var(--mono); letter-spacing: 0.02em;
margin: 1.5rem 0 0; padding-top: 0.5rem;
}
-
+
.archsvg { display: block; }
.archsvg rect { fill: var(--accent-soft); stroke: var(--rule); }
.archsvg .archnode-box rect { stroke: var(--accent); }
.archsvg .archtitle { fill: var(--ink); font: 600 13px var(--sans); }
.archsvg .archsub { fill: var(--muted); font: 400 11px var(--sans); }
-
+
.archsvg .archedge { fill: none; stroke: var(--muted); stroke-width: 1.5; }
-
+
.archsvg .archalarm { fill: var(--good); }
.archsvg .archalarm-firing { fill: var(--bad); }
-
+
.archbox {
border: 1px solid var(--rule); border-radius: 0.6rem;
padding: 0.9rem 1.1rem; margin: 0 0 0.6rem; max-width: 64rem;
}
.archbox h3 { margin: 0 0 0.3rem; }
.archbox p { margin: 0 0 0.2rem; }
-
+
.archbox .fact + .fact::before { content: " · "; color: var(--muted); }
footer {
max-width: 64rem; margin: 0 auto; padding: 1.25rem 0 2rem;
border-top: 1px solid var(--rule);
font-size: 0.85rem; color: var(--muted);
}
-
+
.pagehead { margin: 0 0 1.75rem; }
.eyebrow {
font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em;
@@ -398,9 +398,9 @@
}
.pagehead h1 { margin: 0 0 0.35rem; }
.pagehead .lede { margin: 0; font-size: 1.05rem; }
-
+
.reveal > summary { font-size: 1.1rem; font-weight: 600; cursor: pointer; }
-
+
.tabs {
display: flex; flex-wrap: wrap; gap: 1.25rem; margin: 0 0 2rem;
border-bottom: 1px solid var(--rule);
@@ -415,11 +415,46 @@
header nav a[aria-current] { color: var(--ink); font-weight: 600; text-decoration: none; }
header nav a.addr { color: var(--muted); text-decoration: none; }
header nav a.addr:hover { text-decoration: underline; }
+
+ .billing-layout { max-width: 60rem; }
+ .billing-current { padding: 1.25rem 1.5rem; border: 1px solid var(--rule); border-radius: .85rem; background: var(--accent-soft); margin-bottom: 2rem; }
+ .billing-current h2 { margin-top: 0; }
+ .billing-current p:last-child { margin-bottom: 0; }
+ .billing-layout .billing-quote { max-width: none; display: flex; align-items: end; gap: 1rem; padding: 1rem 0; margin: 0 0 1rem; }
+ .billing-quote label { flex: 1; max-width: 25rem; }
+ .billing-layout select, .billing-layout input[type=text] { display: block; width: 100%; padding: .7rem .8rem; margin-top: .4rem; }
+ .billing-plan-options { border: 0; display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: .85rem; margin: 0 0 1.5rem; padding: 0; }
+ .billing-plan-option { display: flex; gap: .7rem; align-items: start; border: 1px solid var(--rule); border-radius: .8rem; padding: 1.2rem; cursor: pointer; }
+ .billing-plan-option input { margin-top: .35rem; accent-color: var(--accent); flex: none; }
+ .billing-plan-option-body { display: flex; flex-direction: column; gap: .2rem; }
+ .billing-option-price { font-size: 1.8rem; font-weight: 650; letter-spacing: -.04em; margin-top: .5rem; }
+ .billing-plan-option:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: var(--accent-soft); }
+ .billing-plan-option:has(input:focus-visible) { outline: 3px solid var(--accent); outline-offset: 3px; }
+ .billing-plan-option.unavailable { opacity: .55; cursor: not-allowed; }
+ .billing-inclusions { border: 1px solid var(--rule); border-radius: .8rem; margin-bottom: 1rem; }
+ .billing-inclusions table { width: 100%; margin: 0; }
+ .billing-inclusions th, .billing-inclusions td { padding: 1rem; vertical-align: top; }
+ .billing-inclusions th { text-align: left; }
+ .billing-inclusions td:nth-child(2) { min-width: 10rem; }
+ .billing-address { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; margin-top: 1.5rem; }
+ .billing-address label { display: block; font-weight: 550; }
+ .billing-payment { margin-top: 1.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; }
+ .billing-consent { display: block; max-width: 48rem; }
+ .billing-payment button { padding: .8rem 1.3rem; }
+ .billing-payment .small { margin-top: .75rem; }
+ @media (max-width: 640px) {
+ .billing-plan-options, .billing-address { grid-template-columns: 1fr; }
+ .billing-layout .billing-quote { display: block; }
+ .billing-quote button { margin-top: .8rem; }
+ .billing-inclusions table { min-width: 38rem; }
+ .billing-plan-option-body { flex: 1; }
+ .billing-option-price { margin-top: .1rem; }
+ }
</style>
<style>.preview-state{border-top:3px dashed var(--accent);margin:3rem 0 0;padding-top:1rem}.preview-label{font:600 .85rem/1.4 var(--sans);color:var(--accent);text-transform:uppercase;letter-spacing:.04em;max-width:64rem;margin:0 auto .5rem}</style></head>
<body>
<header>
-
+
<details class="switcher" id="switcher">
<summary>
<span class="small">Account</span>
@@ -431,24 +466,24 @@
<li>
<span class="current" aria-current="true"><span class="mark"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>ops</span>
</li>
-
+
</ul>
<hr>
<div class="new"><a href="/accounts/new"><span class="mark"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg></span>New account</a></div>
</div>
</details>
-
+
<nav>
-
+
<a href="/a/ops/">Apps</a>
<a href="/a/ops/fleet">Fleet</a>
<a href="/a/ops/team">Team</a>
<a href="/a/ops/billing" aria-current="page" data-copy="billing.nav">Billing</a>
<a href="/a/ops/settings">Settings</a>
-
+
<a class="addr" href="/settings" title="Your settings">paul@keymail.dev</a>
<form method="post" action="/auth/signout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
<button class="linkish" type="submit">Sign out</button>
</form>
</nav>
@@ -464,12 +499,14 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -478,17 +515,18 @@
+</section>
-<h2><span data-copy="billing.choose.heading">Choose a plan</span></h2>
+<h2 id="billing-choose-heading"><span data-copy="billing.choose.heading">Choose a plan</span></h2>
<p class="small"><span data-copy="billing.choose.lede">Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</span></p>
-<form class="signin" method="get" action="/a/bab/billing">
+<form class="signin billing-quote" method="get" action="/a/bab/billing">
<label><span data-copy="billing.country.label">Billing country</span>
<select name="country">
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE" selected>Ireland</option>
-
+
</select>
</label>
<button class="linkish" type="submit"><span data-copy="billing.quote.button">Show prices for this country</span></button>
@@ -496,58 +534,87 @@
<p class="small">Prices for Ireland are 100% of the base price, and so are the monthly inclusions.</p>
-<form method="post" action="/a/bab/billing/checkout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
- <div class="tablewrap">
+<form class="billing-checkout" method="post" action="/a/bab/billing/checkout">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="personal" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.personal.name">Personal</span></strong>
+ <span class="billing-option-price">€24</span>
+ <span class="small"><span data-copy="billing.price.peryear">a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="community" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.community.name">Community</span></strong>
+ <span class="billing-option-price">€5</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="business" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.business.name">Business</span></strong>
+ <span class="billing-option-price">€50</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="personal" required>
- <span data-copy="billing.plan.personal.name">Personal</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.personal.name">Personal</span></th>
<td><strong>€24</strong> <span data-copy="billing.price.peryear">a year</span></td>
<td class="small"><span data-copy="billing.plan.personal.blurb">One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="community" required>
- <span data-copy="billing.plan.community.name">Community</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.community.name">Community</span></th>
<td><strong>€5</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€50 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.community.blurb">Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="business" required>
- <span data-copy="billing.plan.business.name">Business</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.business.name">Business</span></th>
<td><strong>€50</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€500 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.business.blurb">Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.</span></td>
</tr>
-
+
</table>
</div>
<p class="small"><span data-copy="billing.plan.community.name">Community</span> and <span data-copy="billing.plan.business.name">Business</span> are priced per member. You have 1 members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><span data-copy="billing.country.label">Billing country</span>
<select name="country" required>
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE" selected>Ireland</option>
-
+
</select>
</label></p>
<p><label><span data-copy="billing.postal.label">Postal code</span>
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><span data-copy="billing.postal.help">Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</span></p>
- <p><label><input type="checkbox" name="consent" value="2026-09-07-v1" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="2026-09-07-v1" required>
I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><span data-copy="billing.pay.button">Continue to payment</span></button>
<p class="small"><span data-copy="billing.pay.note">You pay by card at Stripe. Nothing is charged until you confirm there.</span></p>
+ </div>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: After a failed checkout attempt (error=unavailable)</p><main>
@@ -561,12 +628,14 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -575,17 +644,18 @@
+</section>
-<h2><span data-copy="billing.choose.heading">Choose a plan</span></h2>
+<h2 id="billing-choose-heading"><span data-copy="billing.choose.heading">Choose a plan</span></h2>
<p class="small"><span data-copy="billing.choose.lede">Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</span></p>
-<form class="signin" method="get" action="/a/bab/billing">
+<form class="signin billing-quote" method="get" action="/a/bab/billing">
<label><span data-copy="billing.country.label">Billing country</span>
<select name="country">
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label>
<button class="linkish" type="submit"><span data-copy="billing.quote.button">Show prices for this country</span></button>
@@ -593,58 +663,87 @@
-<form method="post" action="/a/bab/billing/checkout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
- <div class="tablewrap">
+<form class="billing-checkout" method="post" action="/a/bab/billing/checkout">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="personal" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.personal.name">Personal</span></strong>
+ <span class="billing-option-price">€24</span>
+ <span class="small"><span data-copy="billing.price.peryear">a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="community" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.community.name">Community</span></strong>
+ <span class="billing-option-price">€5</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="business" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.business.name">Business</span></strong>
+ <span class="billing-option-price">€50</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="personal" required>
- <span data-copy="billing.plan.personal.name">Personal</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.personal.name">Personal</span></th>
<td><strong>€24</strong> <span data-copy="billing.price.peryear">a year</span></td>
<td class="small"><span data-copy="billing.plan.personal.blurb">One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="community" required>
- <span data-copy="billing.plan.community.name">Community</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.community.name">Community</span></th>
<td><strong>€5</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€50 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.community.blurb">Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="business" required>
- <span data-copy="billing.plan.business.name">Business</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.business.name">Business</span></th>
<td><strong>€50</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€500 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.business.blurb">Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.</span></td>
</tr>
-
+
</table>
</div>
<p class="small"><span data-copy="billing.plan.community.name">Community</span> and <span data-copy="billing.plan.business.name">Business</span> are priced per member. You have 1 members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><span data-copy="billing.country.label">Billing country</span>
<select name="country" required>
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label></p>
<p><label><span data-copy="billing.postal.label">Postal code</span>
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><span data-copy="billing.postal.help">Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</span></p>
- <p><label><input type="checkbox" name="consent" value="2026-09-07-v1" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="2026-09-07-v1" required>
I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><span data-copy="billing.pay.button">Continue to payment</span></button>
<p class="small"><span data-copy="billing.pay.note">You pay by card at Stripe. Nothing is charged until you confirm there.</span></p>
+ </div>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: A second checkout while one is settling (error=pending)</p><main>
@@ -658,12 +757,14 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -672,17 +773,18 @@
+</section>
-<h2><span data-copy="billing.choose.heading">Choose a plan</span></h2>
+<h2 id="billing-choose-heading"><span data-copy="billing.choose.heading">Choose a plan</span></h2>
<p class="small"><span data-copy="billing.choose.lede">Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</span></p>
-<form class="signin" method="get" action="/a/bab/billing">
+<form class="signin billing-quote" method="get" action="/a/bab/billing">
<label><span data-copy="billing.country.label">Billing country</span>
<select name="country">
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label>
<button class="linkish" type="submit"><span data-copy="billing.quote.button">Show prices for this country</span></button>
@@ -690,58 +792,87 @@
-<form method="post" action="/a/bab/billing/checkout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
- <div class="tablewrap">
+<form class="billing-checkout" method="post" action="/a/bab/billing/checkout">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="personal" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.personal.name">Personal</span></strong>
+ <span class="billing-option-price">€24</span>
+ <span class="small"><span data-copy="billing.price.peryear">a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="community" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.community.name">Community</span></strong>
+ <span class="billing-option-price">€5</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="business" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.business.name">Business</span></strong>
+ <span class="billing-option-price">€50</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="personal" required>
- <span data-copy="billing.plan.personal.name">Personal</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.personal.name">Personal</span></th>
<td><strong>€24</strong> <span data-copy="billing.price.peryear">a year</span></td>
<td class="small"><span data-copy="billing.plan.personal.blurb">One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="community" required>
- <span data-copy="billing.plan.community.name">Community</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.community.name">Community</span></th>
<td><strong>€5</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€50 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.community.blurb">Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="business" required>
- <span data-copy="billing.plan.business.name">Business</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.business.name">Business</span></th>
<td><strong>€50</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€500 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.business.blurb">Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.</span></td>
</tr>
-
+
</table>
</div>
<p class="small"><span data-copy="billing.plan.community.name">Community</span> and <span data-copy="billing.plan.business.name">Business</span> are priced per member. You have 1 members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><span data-copy="billing.country.label">Billing country</span>
<select name="country" required>
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label></p>
<p><label><span data-copy="billing.postal.label">Postal code</span>
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><span data-copy="billing.postal.help">Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</span></p>
- <p><label><input type="checkbox" name="consent" value="2026-09-07-v1" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="2026-09-07-v1" required>
I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><span data-copy="billing.pay.button">Continue to payment</span></button>
<p class="small"><span data-copy="billing.pay.note">You pay by card at Stripe. Nothing is charged until you confirm there.</span></p>
+ </div>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: The account changed under the form (error=conflict)</p><main>
@@ -755,12 +886,14 @@
<p class="banner error"><span data-copy="billing.error.conflict">The account changed while you were on this page, for example a member was added. Check the plan and try again.</span></p>
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -769,17 +902,18 @@
+</section>
-<h2><span data-copy="billing.choose.heading">Choose a plan</span></h2>
+<h2 id="billing-choose-heading"><span data-copy="billing.choose.heading">Choose a plan</span></h2>
<p class="small"><span data-copy="billing.choose.lede">Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</span></p>
-<form class="signin" method="get" action="/a/bab/billing">
+<form class="signin billing-quote" method="get" action="/a/bab/billing">
<label><span data-copy="billing.country.label">Billing country</span>
<select name="country">
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label>
<button class="linkish" type="submit"><span data-copy="billing.quote.button">Show prices for this country</span></button>
@@ -787,58 +921,87 @@
-<form method="post" action="/a/bab/billing/checkout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
- <div class="tablewrap">
+<form class="billing-checkout" method="post" action="/a/bab/billing/checkout">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="personal" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.personal.name">Personal</span></strong>
+ <span class="billing-option-price">€24</span>
+ <span class="small"><span data-copy="billing.price.peryear">a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="community" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.community.name">Community</span></strong>
+ <span class="billing-option-price">€5</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="business" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.business.name">Business</span></strong>
+ <span class="billing-option-price">€50</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="personal" required>
- <span data-copy="billing.plan.personal.name">Personal</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.personal.name">Personal</span></th>
<td><strong>€24</strong> <span data-copy="billing.price.peryear">a year</span></td>
<td class="small"><span data-copy="billing.plan.personal.blurb">One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="community" required>
- <span data-copy="billing.plan.community.name">Community</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.community.name">Community</span></th>
<td><strong>€5</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€50 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.community.blurb">Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="business" required>
- <span data-copy="billing.plan.business.name">Business</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.business.name">Business</span></th>
<td><strong>€50</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€500 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.business.blurb">Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.</span></td>
</tr>
-
+
</table>
</div>
<p class="small"><span data-copy="billing.plan.community.name">Community</span> and <span data-copy="billing.plan.business.name">Business</span> are priced per member. You have 1 members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><span data-copy="billing.country.label">Billing country</span>
<select name="country" required>
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label></p>
<p><label><span data-copy="billing.postal.label">Postal code</span>
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><span data-copy="billing.postal.help">Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</span></p>
- <p><label><input type="checkbox" name="consent" value="2026-09-07-v1" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="2026-09-07-v1" required>
I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><span data-copy="billing.pay.button">Continue to payment</span></button>
<p class="small"><span data-copy="billing.pay.note">You pay by card at Stripe. Nothing is charged until you confirm there.</span></p>
+ </div>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: Back from Stripe, webhook not yet arrived (pending=1)</p><main>
@@ -852,12 +1015,14 @@
<p class="banner"><span data-copy="billing.current.pending">Thanks. Stripe is confirming your payment. Your plan turns on within a few minutes, and this page will show it.</span></p>
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -866,17 +1031,18 @@
+</section>
-<h2><span data-copy="billing.choose.heading">Choose a plan</span></h2>
+<h2 id="billing-choose-heading"><span data-copy="billing.choose.heading">Choose a plan</span></h2>
<p class="small"><span data-copy="billing.choose.lede">Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</span></p>
-<form class="signin" method="get" action="/a/bab/billing">
+<form class="signin billing-quote" method="get" action="/a/bab/billing">
<label><span data-copy="billing.country.label">Billing country</span>
<select name="country">
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label>
<button class="linkish" type="submit"><span data-copy="billing.quote.button">Show prices for this country</span></button>
@@ -884,58 +1050,87 @@
-<form method="post" action="/a/bab/billing/checkout">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
- <div class="tablewrap">
+<form class="billing-checkout" method="post" action="/a/bab/billing/checkout">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="personal" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.personal.name">Personal</span></strong>
+ <span class="billing-option-price">€24</span>
+ <span class="small"><span data-copy="billing.price.peryear">a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="community" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.community.name">Community</span></strong>
+ <span class="billing-option-price">€5</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ <label class="billing-plan-option">
+ <input type="radio" name="plan" value="business" required>
+ <span class="billing-plan-option-body"><strong><span data-copy="billing.plan.business.name">Business</span></strong>
+ <span class="billing-option-price">€50</span>
+ <span class="small"><span data-copy="billing.price.permember">per member a year</span></span>
+ </span>
+ </label>
+
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="personal" required>
- <span data-copy="billing.plan.personal.name">Personal</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.personal.name">Personal</span></th>
<td><strong>€24</strong> <span data-copy="billing.price.peryear">a year</span></td>
<td class="small"><span data-copy="billing.plan.personal.blurb">One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="community" required>
- <span data-copy="billing.plan.community.name">Community</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.community.name">Community</span></th>
<td><strong>€5</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€50 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.community.blurb">Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.</span></td>
</tr>
-
+
<tr>
- <td><label><input type="radio" name="plan" value="business" required>
- <span data-copy="billing.plan.business.name">Business</span></label></td>
+ <th scope="row"><span data-copy="billing.plan.business.name">Business</span></th>
<td><strong>€50</strong> <span data-copy="billing.price.permember">per member a year</span>
<br><span class="small">€500 <span data-copy="billing.price.peryear">a year</span></span></td>
<td class="small"><span data-copy="billing.plan.business.blurb">Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.</span></td>
</tr>
-
+
</table>
</div>
<p class="small"><span data-copy="billing.plan.community.name">Community</span> and <span data-copy="billing.plan.business.name">Business</span> are priced per member. You have 1 members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><span data-copy="billing.country.label">Billing country</span>
<select name="country" required>
<option value="" data-copy="billing.country.placeholder">Choose a country</option>
<option value="IE">Ireland</option>
-
+
</select>
</label></p>
<p><label><span data-copy="billing.postal.label">Postal code</span>
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><span data-copy="billing.postal.help">Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</span></p>
- <p><label><input type="checkbox" name="consent" value="2026-09-07-v1" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="2026-09-07-v1" required>
I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><span data-copy="billing.pay.button">Continue to payment</span></button>
<p class="small"><span data-copy="billing.pay.note">You pay by card at Stripe. Nothing is charged until you confirm there.</span></p>
+ </div>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: Active Personal subscription, portal available</p><main>
@@ -949,20 +1144,23 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
-<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-09-30.</p>
+<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-10-01.</p>
+</section>
@@ -970,12 +1168,13 @@
<h2><span data-copy="billing.manage.heading">Cards, invoices and cancelling</span></h2>
<p class="small"><span data-copy="billing.manage.body">Change your card, download invoices or cancel at Stripe. Cancelling keeps the plan until the end of the year you paid for.</span></p>
<form method="post" action="/a/bab/billing/portal">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
<button type="submit"><span data-copy="billing.manage.button">Open billing at Stripe</span></button>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: Payment failed on renewal (past_due)</p><main>
@@ -989,20 +1188,23 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
-<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-09-30.</p>
+<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-10-01.</p>
<p class="banner error"><span data-copy="billing.current.pastdue">The last payment did not go through. Update your card below to keep the plan.</span></p>
+</section>
@@ -1010,12 +1212,13 @@
<h2><span data-copy="billing.manage.heading">Cards, invoices and cancelling</span></h2>
<p class="small"><span data-copy="billing.manage.body">Change your card, download invoices or cancel at Stripe. Cancelling keeps the plan until the end of the year you paid for.</span></p>
<form method="post" action="/a/bab/billing/portal">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
<button type="submit"><span data-copy="billing.manage.button">Open billing at Stripe</span></button>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: An operator review hold is open</p><main>
@@ -1029,20 +1232,23 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
-<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-09-30.</p>
+<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-10-01.</p>
<p class="banner error"><span data-copy="billing.current.pastdue">The last payment did not go through. Update your card below to keep the plan.</span></p>
<p class="banner"><span data-copy="billing.current.review">Something about this account's billing needs a person to look at it. Nothing is charged while that happens. We will be in touch.</span></p>
+</section>
@@ -1050,12 +1256,13 @@
<h2><span data-copy="billing.manage.heading">Cards, invoices and cancelling</span></h2>
<p class="small"><span data-copy="billing.manage.body">Change your card, download invoices or cancel at Stripe. Cancelling keeps the plan until the end of the year you paid for.</span></p>
<form method="post" action="/a/bab/billing/portal">
- <input type="hidden" name="csrf" value="kNaVjkqrWoKQCUW0HrmSh6u20nFOLYYgAlZvEDeCt_o">
+ <input type="hidden" name="csrf" value="QyDPBgnssyCMLbG0IoQxSv_nTUejKYIiQZS9X3LSn1Q">
<button type="submit"><span data-copy="billing.manage.button">Open billing at Stripe</span></button>
</form>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: A member who is not an owner</p><main>
@@ -1069,24 +1276,28 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
-<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-09-30.</p>
+<p><span data-copy="billing.plan.personal.name">Personal</span>, 1 members, renews on 2027-10-01.</p>
<p class="banner error"><span data-copy="billing.current.pastdue">The last payment did not go through. Update your card below to keep the plan.</span></p>
<p class="banner"><span data-copy="billing.current.review">Something about this account's billing needs a person to look at it. Nothing is charged while that happens. We will be in touch.</span></p>
+</section>
<p class="small"><span data-copy="billing.member.readonly">Only an account owner can change the plan.</span></p>
+</div>
</main></section>
<section class="preview-state"><p class="preview-label">Preview state: Billing switched off on this deployment (production today)</p><main>
@@ -1100,12 +1311,14 @@
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · ops</p>
<h1>Billing</h1>
<p class="lede"><span data-copy="billing.lede">Your plan, what it includes, and how to change it.</span></p>
</div>
+<section class="billing-current">
<h2><span data-copy="billing.current.heading">Your plan</span></h2>
<p><span data-copy="billing.current.free">Free. One member, 3 apps, 100 awake hours a month, 250 MB of storage and 1 GB of transfer. Apps sleep after 2 minutes idle.</span></p>
@@ -1114,10 +1327,12 @@
+</section>
<p><span data-copy="billing.closed">Paid plans are not open on this console yet. Every account is on the Free plan.</span></p>
+</div>
</main></section></body>
</html>
diff --git a/internal/console/templates/billing.html b/internal/console/templates/billing.html
index 5a9f1a507..4fcc8142e 100644
--- a/internal/console/templates/billing.html
+++ b/internal/console/templates/billing.html
@@ -12,12 +12,14 @@
{{if eq .Error "conflict"}}<p class="banner error"><!-- billing.error.conflict -->The account changed while you were on this page, for example a member was added. Check the plan and try again.</p>{{end}}
{{if .PendingCheckout}}<p class="banner"><!-- billing.current.pending -->Thanks. Stripe is confirming your payment. Your plan turns on within a few minutes, and this page will show it.</p>{{end}}
{{end}}
+<div class="billing-layout">
<div class="pagehead">
<p class="eyebrow">Account · {{.Account.Name}}</p>
<h1><!-- billing.title -->Billing</h1>
<p class="lede"><!-- billing.lede -->Your plan, what it includes, and how to change it.</p>
</div>
{{with .Billing}}
+<section class="billing-current">
<h2><!-- billing.current.heading -->Your plan</h2>
{{if or .Active .PastDue}}
<p><!-- billing.current.active -->{{.PlanName}}, {{.Quantity}} members, renews on {{.Renews}}.</p>
@@ -29,16 +31,17 @@
{{if .PastDue}}<p class="banner error"><!-- billing.current.pastdue -->The last payment did not go through. Update your card below to keep the plan.</p>{{end}}
{{if .Review}}<p class="banner"><!-- billing.current.review -->Something about this account's billing needs a person to look at it. Nothing is charged while that happens. We will be in touch.</p>{{end}}
+</section>
{{if .Closed}}
<p><!-- billing.closed -->Paid plans are not open on this console yet. Every account is on the Free plan.</p>
{{else if not .Owner}}
<p class="small"><!-- billing.member.readonly -->Only an account owner can change the plan.</p>
{{else}}
{{if .CanChoose}}
-<h2><!-- billing.choose.heading -->Choose a plan</h2>
+<h2 id="billing-choose-heading"><!-- billing.choose.heading -->Choose a plan</h2>
<p class="small"><!-- billing.choose.lede -->Prices are per year, before VAT, and depend on your billing country. The exact price is shown before you pay.</p>
-<form class="signin" method="get" action="{{$.Base}}/billing">
+<form class="signin billing-quote" method="get" action="{{$.Base}}/billing">
<label><!-- billing.country.label -->Billing country
<select name="country">
<option value=""><!-- billing.country.placeholder -->Choose a country</option>
@@ -51,14 +54,24 @@
{{if .QuoteError}}<p class="small"><!-- billing.quote.unknown (not in strings.json) -->There are no prices for that country. Choose one from the list.</p>{{end}}
{{with .Quote}}<p class="small"><!-- billing.quote.band -->Prices for {{.Country}} are {{.Percent}}% of the base price, and so are the monthly inclusions.</p>{{end}}
-<form method="post" action="{{$.Base}}/billing/checkout">
+<form class="billing-checkout" method="post" action="{{$.Base}}/billing/checkout">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
- <div class="tablewrap">
+ <fieldset class="billing-plan-options" aria-labelledby="billing-choose-heading">
+ {{range .Plans}}
+ <label class="billing-plan-option{{if .Unavailable}} unavailable{{end}}">
+ <input type="radio" name="plan" value="{{.Plan}}" required{{if .Unavailable}} disabled{{end}}>
+ <span class="billing-plan-option-body"><strong>{{.Name}}</strong>
+ <span class="billing-option-price">{{if .Team}}{{.PerMember}}{{else}}{{.Price}}{{end}}</span>
+ <span class="small">{{if .Team}}per member a year{{else}}a year{{end}}</span>
+ {{if .Unavailable}}<span class="small">Personal is for one member. This account has {{$.Billing.Members}}.</span>{{end}}</span>
+ </label>
+ {{end}}
+ </fieldset>
+ <div class="tablewrap billing-inclusions">
<table>
{{range .Plans}}
<tr>
- <td><label><input type="radio" name="plan" value="{{.Plan}}" required{{if .Unavailable}} disabled{{end}}>
- {{if eq .Plan "personal"}}<!-- billing.plan.personal.name -->Personal{{else if eq .Plan "community"}}<!-- billing.plan.community.name -->Community{{else if eq .Plan "business"}}<!-- billing.plan.business.name -->Business{{end}}</label></td>
+ <th scope="row">{{if eq .Plan "personal"}}<!-- billing.plan.personal.name -->Personal{{else if eq .Plan "community"}}<!-- billing.plan.community.name -->Community{{else if eq .Plan "business"}}<!-- billing.plan.business.name -->Business{{end}}</th>
<td>{{if .Team}}<strong>{{.PerMember}}</strong> <!-- billing.price.permember -->per member a year
<br><span class="small">{{.Price}} <!-- billing.price.peryear -->a year</span>{{else}}<strong>{{.Price}}</strong> <!-- billing.price.peryear -->a year{{end}}</td>
<td class="small">{{if eq .Plan "personal"}}<!-- billing.plan.personal.blurb -->One member. Each month: 85 awake hours, 2 GB storage, 2 GB transfer.{{else if eq .Plan "community"}}<!-- billing.plan.community.blurb -->Per member, minimum 10. Each member adds, per month: 17 awake hours, 0.5 GB storage, 0.4 GB transfer.{{else if eq .Plan "business"}}<!-- billing.plan.business.blurb -->Per member, minimum 10. Each member adds, per month: 170 awake hours, 5 GB storage, 3 GB transfer, 30,000 replication writes. Priority support.{{end}}{{if and .Unavailable (eq .Plan "personal")}}<br><!-- billing.plan.personal.unavailable -->Personal is for one member. This account has {{$.Billing.Members}}.{{end}}</td>
@@ -67,6 +80,7 @@
</table>
</div>
<p class="small"><!-- billing.members.note -->Community and Business are priced per member. You have {{.Members}} members; the minimum is 10.</p>
+ <div class="billing-address">
<p><label><!-- billing.country.label -->Billing country
<select name="country" required>
<option value=""><!-- billing.country.placeholder -->Choose a country</option>
@@ -77,11 +91,14 @@
<p><label><!-- billing.postal.label -->Postal code
<input type="text" name="postal" autocomplete="postal-code">
</label></p>
+ </div>
<p class="small"><!-- billing.postal.help -->Needed in the United States, Canada and India, where tax depends on it. Leave it empty elsewhere. We do not keep it.</p>
- <p><label><input type="checkbox" name="consent" value="{{.TermsVersion}}" required>
+ <div class="billing-payment">
+ <p><label class="billing-consent"><input type="checkbox" name="consent" value="{{.TermsVersion}}" required>
<!-- billing.consent.label, with billing.consent.link inside -->I have read <a href="https://carloku.com/pricing/">the pricing page</a> and agree to be charged yearly at the price shown at checkout, plus usage beyond the plan's inclusions billed monthly.</label></p>
<button type="submit"><!-- billing.pay.button -->Continue to payment</button>
<p class="small"><!-- billing.pay.note -->You pay by card at Stripe. Nothing is charged until you confirm there.</p>
+ </div>
</form>
{{end}}
@@ -95,4 +112,5 @@
{{end}}
{{end}}
{{end}}
+</div>
{{template "bottom" .}}{{end}}
diff --git a/internal/console/templates/layout.html b/internal/console/templates/layout.html
index 31b085b5d..46ea480fb 100644
--- a/internal/console/templates/layout.html
+++ b/internal/console/templates/layout.html
@@ -533,6 +533,41 @@
header nav a[aria-current] { color: var(--ink); font-weight: 600; text-decoration: none; }
header nav a.addr { color: var(--muted); text-decoration: none; }
header nav a.addr:hover { text-decoration: underline; }
+
+ .billing-layout { max-width: 60rem; }
+ .billing-current { padding: 1.25rem 1.5rem; border: 1px solid var(--rule); border-radius: .85rem; background: var(--accent-soft); margin-bottom: 2rem; }
+ .billing-current h2 { margin-top: 0; }
+ .billing-current p:last-child { margin-bottom: 0; }
+ .billing-layout .billing-quote { max-width: none; display: flex; align-items: end; gap: 1rem; padding: 1rem 0; margin: 0 0 1rem; }
+ .billing-quote label { flex: 1; max-width: 25rem; }
+ .billing-layout select, .billing-layout input[type=text] { display: block; width: 100%; padding: .7rem .8rem; margin-top: .4rem; }
+ .billing-plan-options { border: 0; display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: .85rem; margin: 0 0 1.5rem; padding: 0; }
+ .billing-plan-option { display: flex; gap: .7rem; align-items: start; border: 1px solid var(--rule); border-radius: .8rem; padding: 1.2rem; cursor: pointer; }
+ .billing-plan-option input { margin-top: .35rem; accent-color: var(--accent); flex: none; }
+ .billing-plan-option-body { display: flex; flex-direction: column; gap: .2rem; }
+ .billing-option-price { font-size: 1.8rem; font-weight: 650; letter-spacing: -.04em; margin-top: .5rem; }
+ .billing-plan-option:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: var(--accent-soft); }
+ .billing-plan-option:has(input:focus-visible) { outline: 3px solid var(--accent); outline-offset: 3px; }
+ .billing-plan-option.unavailable { opacity: .55; cursor: not-allowed; }
+ .billing-inclusions { border: 1px solid var(--rule); border-radius: .8rem; margin-bottom: 1rem; }
+ .billing-inclusions table { width: 100%; margin: 0; }
+ .billing-inclusions th, .billing-inclusions td { padding: 1rem; vertical-align: top; }
+ .billing-inclusions th { text-align: left; }
+ .billing-inclusions td:nth-child(2) { min-width: 10rem; }
+ .billing-address { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; margin-top: 1.5rem; }
+ .billing-address label { display: block; font-weight: 550; }
+ .billing-payment { margin-top: 1.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; }
+ .billing-consent { display: block; max-width: 48rem; }
+ .billing-payment button { padding: .8rem 1.3rem; }
+ .billing-payment .small { margin-top: .75rem; }
+ @media (max-width: 640px) {
+ .billing-plan-options, .billing-address { grid-template-columns: 1fr; }
+ .billing-layout .billing-quote { display: block; }
+ .billing-quote button { margin-top: .8rem; }
+ .billing-inclusions table { min-width: 38rem; }
+ .billing-plan-option-body { flex: 1; }
+ .billing-option-price { margin-top: .1rem; }
+ }
</style>
</head>
<body>