pricing: land £7/£70 paid tier and make behaviour match
Marketing + behaviour pass to get the site ready for Paddle approval.
Pricing page
- £7/month, £70/year headline (was "Coming soon").
- Bigger tier names (was 11px uppercase mono — looked like chips).
- Real CTAs (button base styles were only scoped to .hero__ctas).
- "Best value" badge + drop-shadow on the Paid card; full-width
block CTAs that align across both cards.
- "Free vs Paid at a glance" comparison table beneath the cards.
- Compact "Invite a friend — both get 50% off for 3 months"
callout with the detail explanation behind a <dialog> popup.
Tier copy + behaviour now consistent
- Free strategic-log refresh is every 6 hours, not hourly. New
read-side filter on /api/log/{latest,by-date} restricts free
users to logs at boundary hours (00/06/12/18 UTC); paid users
still see the most recent.
- Follow-up chat is paid-only. /api/chat returns 402 for free;
the chat sidebar on /log is replaced with a locked aside and
chat.js no longer loads at all for free users.
- Dashboard meta lines + landing copy softened so they no longer
promise hourly to everyone.
Future-proofing copy on public pages
- Dropped "free forever" wording (we may close the free tier).
- "Trading 212 CSV" became "broker CSV (Trading 212 today; more
planned)" on pricing + landing; the actual import UIs stay
T212-specific.
Terms
- Renamed Terms of Service -> Terms and Conditions (Paddle
expectation), bumped last-updated to 2026-05-26.
- New §6 Refunds covering the 14-day cooling off, post-window
cancellation, termination-by-us refunds, statutory rights, and
how to request a refund.
- Renumbered §7-§14 and fixed the disclaimer link labels.
Tests
- 6 new tests in tests/test_chat_and_log_gates.py cover the
chat 402 + the boundary-hour filter on both log endpoints.
- Full suite: 205 passed, 5 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
70cf6148ce
commit
2297f9b2ed
11 changed files with 757 additions and 117 deletions
|
|
@ -681,6 +681,25 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
|||
.log-page__cal { padding: 10px; }
|
||||
.log-page__content { min-height: 60vh; }
|
||||
.log-page__chat { padding: 8px; min-height: 60vh; display: flex; flex-direction: column; }
|
||||
.log-page__chat--locked { opacity: 0.92; }
|
||||
.chat-locked {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
padding: 24px 18px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 4px;
|
||||
margin: 8px 4px;
|
||||
}
|
||||
.chat-locked p { margin: 0; max-width: 280px; }
|
||||
.chat-locked strong { color: var(--text); display: block; margin-bottom: 6px; }
|
||||
|
||||
/* --- Calendar widget --------------------------------------------------- */
|
||||
|
||||
|
|
@ -1527,15 +1546,24 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
|||
margin: 0 0 24px;
|
||||
}
|
||||
.hero__ctas { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.hero__ctas .btn-primary,
|
||||
.hero__ctas .btn-secondary {
|
||||
/* Shared button shape — was previously scoped to .hero__ctas, which made
|
||||
the pricing-card CTAs render as bare anchors. */
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
display: inline-block;
|
||||
padding: 10px 22px;
|
||||
border-radius: 3px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Block variant: full-width within parent, slightly taller — used inside
|
||||
tier cards so each CTA spans the card and reads as the obvious action. */
|
||||
.btn-block { display: block; width: 100%; padding: 12px 22px; font-size: 14px; }
|
||||
|
||||
/* Qualify with `a` so we beat `a { color: var(--accent) }` and any
|
||||
:link/:visited UA defaults. Without `a.btn-primary` the cascade can
|
||||
resolve in favour of the visited-link color on some browsers and the
|
||||
|
|
@ -1665,58 +1693,164 @@ a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
|
|||
|
||||
.tier-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 18px;
|
||||
margin: 8px 0 24px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 8px 0 40px;
|
||||
}
|
||||
.tier-card {
|
||||
position: relative;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 22px 22px 26px;
|
||||
border-radius: 6px;
|
||||
padding: 28px 26px 28px;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.tier-card--featured {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent) inset;
|
||||
box-shadow: 0 0 0 1px var(--accent) inset,
|
||||
0 12px 32px rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
.tier-card__name {
|
||||
[data-theme="dark"] .tier-card--featured {
|
||||
box-shadow: 0 0 0 1px var(--accent) inset,
|
||||
0 12px 32px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.tier-card__badge {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
left: 24px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
/* Tier name — the actual heading, not the small uppercase chip it used
|
||||
to be. Pairs with .tier-card__tagline for a one-line value framing. */
|
||||
.tier-card__name {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
margin: 0 0 4px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.tier-card__tagline {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.tier-card__price {
|
||||
font-size: 22px;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.tier-card__price-unit {
|
||||
font-size: 15px;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.tier-card__price-hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 18px;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tier-card__divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
.tier-card__list-head {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.tier-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 22px;
|
||||
margin: 0 0 24px;
|
||||
flex: 1;
|
||||
}
|
||||
.tier-card li {
|
||||
font-size: 13.5px;
|
||||
color: var(--text);
|
||||
padding: 6px 0;
|
||||
line-height: 1.55;
|
||||
padding: 8px 0 8px 22px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.tier-card li:last-child { border-bottom: 0; }
|
||||
.tier-card li::before { content: "✓ "; color: var(--positive); font-weight: 700; margin-right: 4px; }
|
||||
.tier-card li.tier-card__excluded { color: var(--muted); }
|
||||
.tier-card li.tier-card__excluded::before { content: "✕ "; color: var(--dim); }
|
||||
.tier-card__cta { margin-top: auto; }
|
||||
.tier-card li::before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
color: var(--positive);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tier-card__cta { margin-top: 18px; }
|
||||
|
||||
.tier-card__more {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* Side-by-side feature comparison table. Lives below the cards and
|
||||
makes the deltas readable at a glance — the cards sell, the table
|
||||
confirms. */
|
||||
.compare-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 16px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.compare-table th,
|
||||
.compare-table td {
|
||||
text-align: left;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.compare-table thead th {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.compare-table th[scope="row"] {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
width: 38%;
|
||||
}
|
||||
.compare-table td.compare-table__free { color: var(--muted); }
|
||||
.compare-table td.compare-table__paid { color: var(--text); font-weight: 500; }
|
||||
.compare-table td.compare-table__paid strong { color: var(--accent); font-weight: 600; }
|
||||
.compare-table td.compare-table__none { color: var(--dim); }
|
||||
@media (max-width: 520px) {
|
||||
.compare-table th[scope="row"] { width: 50%; }
|
||||
.compare-table th, .compare-table td { padding: 10px 8px; font-size: 13px; }
|
||||
}
|
||||
|
||||
/* BETA indicator pill in the app header — see app/templates/base.html. */
|
||||
.beta-chip {
|
||||
|
|
@ -1908,3 +2042,127 @@ a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
|
|||
color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* --- Invite-a-friend callout (pricing) ----------------------------- */
|
||||
/* A single-row visual banner that names the offer at a glance. The
|
||||
detail text lives in a <dialog> behind the "How it works" button to
|
||||
keep the pricing page scannable. */
|
||||
.invite-callout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px 24px;
|
||||
margin: 0 0 40px;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--accent) 12%, var(--surface)),
|
||||
var(--surface));
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
|
||||
border-left: 4px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.invite-callout__icon {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
filter: saturate(1.1);
|
||||
}
|
||||
.invite-callout__body { flex: 1; min-width: 0; }
|
||||
.invite-callout__eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invite-callout__headline {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invite-callout__headline strong { color: var(--accent); font-weight: 700; }
|
||||
.invite-callout__sub {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.invite-callout .btn-secondary { flex-shrink: 0; }
|
||||
@media (max-width: 560px) {
|
||||
.invite-callout { flex-direction: column; align-items: flex-start; gap: 14px; }
|
||||
.invite-callout .btn-secondary { width: 100%; }
|
||||
}
|
||||
|
||||
/* Generic text-only modal — reuse for any "click for the details"
|
||||
pattern. Same <dialog> mechanics as .shot-modal. */
|
||||
.text-modal {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
max-width: min(92vw, 560px);
|
||||
max-height: 88vh;
|
||||
padding: 28px 28px 24px;
|
||||
border-radius: 6px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.text-modal::backdrop { background: rgba(0, 0, 0, 0.65); }
|
||||
.text-modal__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 14px;
|
||||
padding-right: 36px;
|
||||
color: var(--text);
|
||||
}
|
||||
.text-modal__head {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.10em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
margin: 20px 0 8px;
|
||||
}
|
||||
.text-modal p {
|
||||
font-size: 13.5px;
|
||||
color: var(--text);
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.text-modal__list {
|
||||
margin: 0 0 8px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
.text-modal__list li {
|
||||
font-size: 13.5px;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.text-modal code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
background: var(--surface-2);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.text-modal__close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--muted);
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.text-modal__close:hover,
|
||||
.text-modal__close:focus-visible {
|
||||
color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue