Paywall — Blur-locked premium content
A Northwind Analytics weekly report where premium sections sit behind a CSS-blurred frosted veil and a centered lock card. Traffic stays visible while revenue, retention, and funnel cards render as legible-but-unreadable SVG bar charts and tables underneath the blur. An Unlock with Pro button smoothly removes the blur, swaps masked digits for the real figures, and confirms with a toast, while a Compare plans modal offers Starter, Pro, and Scale tiers with a monthly/annual billing toggle.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-md: 0 8px 24px rgba(16, 19, 34, 0.08);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
max-width: 880px;
margin: 0 auto;
padding: 0 20px 80px;
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 14px;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 10px 16px;
cursor: pointer;
white-space: nowrap;
transition: transform 0.12s ease, background 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.btn-primary {
background: var(--brand);
color: #fff;
box-shadow: var(--sh-sm);
}
.btn-primary:hover {
background: var(--brand-d);
}
.btn-lg {
padding: 13px 24px;
font-size: 15px;
}
.btn-outline {
background: transparent;
color: var(--brand-d);
border-color: var(--brand);
}
.btn-outline:hover {
background: var(--brand-50);
}
.btn-link {
background: none;
border: none;
color: var(--muted);
font-size: 13px;
padding: 6px 8px;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
font-weight: 600;
}
.btn-link:hover {
color: var(--ink-2);
}
.btn-link:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: var(--r-sm);
background: transparent;
border: 1px solid transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease;
}
.icon-btn:hover {
background: var(--bg);
color: var(--ink);
}
.icon-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 16px;
padding: 18px 0 14px;
margin-bottom: 6px;
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 16px;
letter-spacing: -0.01em;
}
.brand em {
font-style: normal;
font-weight: 600;
color: var(--muted);
}
.brand-mark {
display: inline-flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
border-radius: 9px;
background: linear-gradient(135deg, var(--brand), var(--brand-700));
color: #fff;
}
.plan-pill {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 6px 14px;
transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
.plan-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
transition: background 0.3s ease;
}
.plan-pill.is-pro {
background: var(--brand-50);
border-color: var(--brand);
color: var(--brand-700);
}
.plan-pill.is-pro .plan-dot {
background: var(--accent);
}
/* ---------- Report header ---------- */
.report-head {
padding: 26px 0 18px;
}
.kicker {
margin: 0 0 6px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-d);
}
.title {
margin: 0 0 10px;
font-size: clamp(26px, 5vw, 36px);
line-height: 1.1;
letter-spacing: -0.02em;
font-weight: 800;
}
.lede {
margin: 0;
font-size: 16px;
color: var(--ink-2);
max-width: 56ch;
}
.lede strong {
color: var(--ink);
}
/* ---------- KPI strip ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-bottom: 28px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
box-shadow: var(--sh-sm);
}
.kpi-label {
margin: 0 0 6px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.kpi-value {
margin: 0 0 4px;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.kpi-delta {
margin: 0;
font-size: 12px;
font-weight: 600;
}
.kpi-delta.is-up {
color: var(--ok);
}
.kpi-delta.is-down {
color: var(--danger);
}
.kpi-locked .kpi-value,
.kpi-locked .kpi-delta {
filter: blur(5px);
user-select: none;
transition: filter 0.5s ease;
}
.kpis.is-unlocked .kpi-locked .kpi-value,
.kpis.is-unlocked .kpi-locked .kpi-delta {
filter: blur(0);
}
/* ---------- Premium grid ---------- */
.premium {
position: relative;
}
.premium-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
filter: blur(8px);
transform: scale(1.012);
transition: filter 0.55s ease, transform 0.55s ease;
}
.premium.is-unlocked .premium-grid {
filter: blur(0);
transform: none;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px;
box-shadow: var(--sh-sm);
}
.card-wide {
grid-column: 1 / -1;
}
.card-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.card-title {
margin: 0;
font-size: 15px;
font-weight: 700;
}
.card-tag {
margin-left: auto;
font-size: 11px;
font-weight: 600;
color: var(--muted);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 9px;
}
.chart {
width: 100%;
}
.chart svg {
display: block;
}
.grid-line {
stroke: var(--line);
stroke-width: 1;
}
.bar {
fill: var(--brand);
}
.bar-accent {
fill: var(--accent);
}
.legend {
list-style: none;
margin: 14px 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 14px;
font-size: 12px;
color: var(--muted);
}
.legend li {
display: inline-flex;
align-items: center;
gap: 6px;
}
.swatch {
width: 10px;
height: 10px;
border-radius: 3px;
background: var(--brand);
}
.swatch-accent {
background: var(--accent);
}
/* ---------- Data table ---------- */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th,
.data-table td {
text-align: left;
padding: 9px 8px;
border-bottom: 1px solid var(--line);
}
.data-table th {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.data-table td {
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.data-table td:first-child {
color: var(--ink);
font-weight: 600;
}
.data-table tr:last-child td {
border-bottom: none;
}
/* ---------- Lock overlay ---------- */
.lock-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
z-index: 5;
transition: opacity 0.45s ease, visibility 0.45s ease;
}
.premium.is-unlocked .lock-overlay {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.lock-card {
text-align: center;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
box-shadow: 0 18px 48px rgba(16, 19, 34, 0.18);
padding: 30px 28px 22px;
max-width: 380px;
width: 100%;
}
.lock-badge {
display: inline-flex;
width: 54px;
height: 54px;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--brand-50);
color: var(--brand-d);
margin-bottom: 14px;
}
.lock-title {
margin: 0 0 8px;
font-size: 20px;
letter-spacing: -0.01em;
}
.lock-copy {
margin: 0 0 18px;
color: var(--muted);
font-size: 14px;
}
.lock-copy strong {
color: var(--ink);
}
.lock-card .btn-lg {
width: 100%;
}
.lock-card .btn-link {
margin-top: 8px;
}
/* ---------- Overlay + modal ---------- */
.overlay {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(16, 19, 34, 0.45);
backdrop-filter: blur(2px);
animation: fade 0.2s ease both;
}
@keyframes fade {
from {
opacity: 0;
}
}
.modal {
position: fixed;
z-index: 41;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(680px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: 0 24px 60px rgba(16, 19, 34, 0.28);
padding: 24px;
animation: pop 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes pop {
from {
opacity: 0;
transform: translate(-50%, -46%) scale(0.97);
}
}
.modal-head {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 18px;
}
.modal-title {
margin: 0;
font-size: 22px;
letter-spacing: -0.01em;
}
.modal-sub {
margin: 4px 0 0;
color: var(--muted);
font-size: 14px;
}
.modal-head .icon-btn {
margin-left: auto;
flex: none;
}
.cycle {
display: inline-flex;
padding: 4px;
gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
margin-bottom: 20px;
}
.cycle-opt {
border: none;
background: transparent;
font: inherit;
font-weight: 600;
font-size: 13px;
color: var(--muted);
padding: 8px 16px;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.16s ease, color 0.16s ease;
}
.cycle-opt:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.cycle-opt.is-active {
background: var(--white);
color: var(--ink);
box-shadow: var(--sh-sm);
}
.cycle-save {
font-size: 11px;
font-weight: 700;
color: var(--ok);
background: rgba(47, 158, 111, 0.12);
padding: 2px 7px;
border-radius: 999px;
}
.plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.plan {
position: relative;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px 16px;
display: flex;
flex-direction: column;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.plan:hover {
border-color: var(--line-2);
box-shadow: var(--sh-sm);
transform: translateY(-2px);
}
.plan-popular {
border-color: var(--brand);
box-shadow: 0 0 0 1px var(--brand), var(--sh-md);
}
.plan-badge {
position: absolute;
top: -11px;
left: 50%;
transform: translateX(-50%);
background: var(--brand);
color: #fff;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
}
.plan-name {
margin: 0;
font-size: 16px;
font-weight: 700;
}
.plan-desc {
margin: 4px 0 14px;
font-size: 13px;
color: var(--muted);
min-height: 34px;
}
.plan-price {
margin: 0 0 16px;
display: flex;
align-items: baseline;
gap: 3px;
}
.plan-price .amount {
font-size: 30px;
font-weight: 800;
letter-spacing: -0.02em;
}
.plan-price .per {
font-size: 13px;
color: var(--muted);
font-weight: 600;
}
.features {
list-style: none;
margin: 0 0 18px;
padding: 0;
display: flex;
flex-direction: column;
gap: 9px;
flex: 1;
}
.features li {
position: relative;
padding-left: 24px;
font-size: 13px;
color: var(--ink-2);
}
.features li::before {
content: "";
position: absolute;
left: 0;
top: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-soft)
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2300b4a6' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 4 4 10-11'/%3E%3C/svg%3E")
center / 11px no-repeat;
}
.plan-cta {
width: 100%;
}
.modal-foot {
margin: 18px 0 0;
text-align: center;
font-size: 12px;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
z-index: 60;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--ink);
color: #fff;
font-size: 14px;
font-weight: 500;
padding: 12px 18px;
border-radius: 999px;
box-shadow: var(--sh-md);
opacity: 0;
transform: translateY(14px);
transition: opacity 0.26s ease, transform 0.26s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.is-in {
opacity: 1;
transform: translateY(0);
}
.toast-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
flex: none;
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.page {
padding: 0 14px 64px;
}
.kpis {
grid-template-columns: 1fr;
gap: 10px;
}
.premium-grid {
grid-template-columns: 1fr;
}
.lock-card {
padding: 26px 20px 20px;
}
.plans {
grid-template-columns: 1fr;
}
.plan-popular {
order: -1;
}
.plan-desc {
min-height: 0;
}
.data-table th:nth-child(2),
.data-table td:nth-child(2) {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg) {
if (!toastWrap) return;
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
var dot = document.createElement("span");
dot.className = "toast-dot";
el.appendChild(dot);
var text = document.createElement("span");
text.textContent = msg;
el.appendChild(text);
toastWrap.appendChild(el);
// Force reflow so the entrance transition runs.
void el.offsetWidth;
el.classList.add("is-in");
window.setTimeout(function () {
el.classList.remove("is-in");
window.setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 280);
}, 2600);
}
/* ---------- Element refs ---------- */
var premium = document.querySelector(".premium");
var premiumGrid = document.getElementById("premiumGrid");
var kpis = document.querySelector(".kpis");
var lockOverlay = document.getElementById("lockOverlay");
var unlockBtn = document.getElementById("unlockBtn");
var plansBtn = document.getElementById("plansBtn");
var planPill = document.getElementById("planPill");
var planPillText = document.getElementById("planPillText");
var overlay = document.getElementById("overlay");
var modal = document.getElementById("modal");
var modalClose = document.getElementById("modalClose");
var unlocked = false;
var lastFocus = null;
/* ---------- Unlock (reveal premium content) ---------- */
function revealRealNumbers() {
var els = document.querySelectorAll("[data-real]");
Array.prototype.forEach.call(els, function (el) {
el.textContent = el.getAttribute("data-real");
});
}
function unlock(planName) {
if (unlocked) {
toast("Already on Northwind Pro");
return;
}
unlocked = true;
revealRealNumbers();
if (premium) premium.classList.add("is-unlocked");
if (kpis) kpis.classList.add("is-unlocked");
if (premiumGrid) premiumGrid.setAttribute("aria-hidden", "false");
if (planPill) planPill.classList.add("is-pro");
if (planPillText) planPillText.textContent = (planName || "Pro") + " plan";
if (unlockBtn) {
unlockBtn.disabled = true;
unlockBtn.textContent = "Unlocked";
}
toast("Unlocked — full report revealed (demo)");
}
if (unlockBtn) {
unlockBtn.addEventListener("click", function () {
unlock("Pro");
});
}
/* ---------- Plans modal ---------- */
function openModal() {
if (!modal || !overlay) return;
lastFocus = document.activeElement;
overlay.hidden = false;
modal.hidden = false;
if (modalClose) {
window.requestAnimationFrame(function () {
try {
modalClose.focus();
} catch (e) {}
});
}
}
function closeModal() {
if (!modal || !overlay) return;
modal.hidden = true;
overlay.hidden = true;
if (lastFocus && typeof lastFocus.focus === "function") {
try {
lastFocus.focus();
} catch (e) {}
}
}
if (plansBtn) plansBtn.addEventListener("click", openModal);
if (modalClose) modalClose.addEventListener("click", closeModal);
if (overlay) overlay.addEventListener("click", closeModal);
// Focus trap within the modal.
if (modal) {
modal.addEventListener("keydown", function (ev) {
if (ev.key !== "Tab") return;
var focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (ev.shiftKey && document.activeElement === first) {
ev.preventDefault();
last.focus();
} else if (!ev.shiftKey && document.activeElement === last) {
ev.preventDefault();
first.focus();
}
});
}
/* ---------- Billing cycle toggle ---------- */
var cycleOpts = Array.prototype.slice.call(
document.querySelectorAll(".cycle-opt")
);
var amounts = Array.prototype.slice.call(
document.querySelectorAll(".plan-price .amount")
);
var pers = Array.prototype.slice.call(document.querySelectorAll(".per"));
var cycleNote = document.getElementById("cycleNote");
function setCycle(cycle) {
cycleOpts.forEach(function (opt) {
var on = opt.getAttribute("data-cycle") === cycle;
opt.classList.toggle("is-active", on);
opt.setAttribute("aria-pressed", String(on));
});
amounts.forEach(function (a) {
var val = a.getAttribute(cycle === "annual" ? "data-annual" : "data-monthly");
if (val) a.textContent = val;
});
pers.forEach(function (p) {
p.textContent = cycle === "annual" ? "/mo, billed yearly" : "/mo";
});
if (cycleNote) cycleNote.textContent = cycle === "annual" ? "annually" : "monthly";
}
cycleOpts.forEach(function (opt) {
opt.addEventListener("click", function () {
setCycle(opt.getAttribute("data-cycle"));
});
});
/* ---------- Plan CTAs ---------- */
var planCtas = Array.prototype.slice.call(
document.querySelectorAll(".plan-cta")
);
planCtas.forEach(function (cta) {
cta.addEventListener("click", function () {
var plan = cta.getAttribute("data-plan");
if (cta.hasAttribute("data-unlock")) {
closeModal();
unlock(plan);
} else {
toast("You're already on the " + plan + " plan");
}
});
});
/* ---------- Esc closes the modal ---------- */
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && modal && !modal.hidden) {
closeModal();
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Blur-locked Premium Report</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 17 9 11l4 4 8-9" />
<path d="M21 6v6h-6" />
</svg>
</span>
<span class="brand-name">Northwind <em>Analytics</em></span>
</div>
<span class="plan-pill" id="planPill">
<span class="plan-dot" aria-hidden="true"></span>
<span id="planPillText">Free plan</span>
</span>
</header>
<!-- Report header -->
<section class="report-head">
<p class="kicker">Weekly Report · Jun 7 – Jun 13, 2026</p>
<h1 class="title">Growth & Revenue Overview</h1>
<p class="lede">Your free snapshot covers traffic. Revenue, retention, and conversion
breakdowns are part of <strong>Northwind Pro</strong>.</p>
</section>
<!-- KPI strip: first one free, rest locked -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<p class="kpi-label">Visitors</p>
<p class="kpi-value">48,210</p>
<p class="kpi-delta is-up">▲ 12.4% vs last week</p>
</article>
<article class="kpi kpi-locked" data-locked>
<p class="kpi-label">Revenue</p>
<p class="kpi-value" data-real="$92,480">$••,•••</p>
<p class="kpi-delta is-up" data-real="▲ 8.1% vs last week">▲ ••• vs last week</p>
</article>
<article class="kpi kpi-locked" data-locked>
<p class="kpi-label">Conversion</p>
<p class="kpi-value" data-real="3.74%">•.••%</p>
<p class="kpi-delta is-down" data-real="▼ 0.3% vs last week">▼ ••• vs last week</p>
</article>
</section>
<!-- Premium report grid (blurred behind overlay) -->
<section class="premium" aria-label="Premium report sections">
<div class="premium-grid" id="premiumGrid" aria-hidden="true">
<!-- Revenue by channel -->
<article class="card" data-blur>
<header class="card-head">
<h2 class="card-title">Revenue by channel</h2>
<span class="card-tag">Last 7 days</span>
</header>
<div class="chart" role="img" aria-label="Bar chart of revenue by channel">
<svg viewBox="0 0 280 140" width="100%" height="140" preserveAspectRatio="none">
<line class="grid-line" x1="0" y1="35" x2="280" y2="35" />
<line class="grid-line" x1="0" y1="70" x2="280" y2="70" />
<line class="grid-line" x1="0" y1="105" x2="280" y2="105" />
<rect class="bar" x="14" y="50" width="34" height="90" rx="4" />
<rect class="bar" x="68" y="28" width="34" height="112" rx="4" />
<rect class="bar" x="122" y="74" width="34" height="66" rx="4" />
<rect class="bar" x="176" y="96" width="34" height="44" rx="4" />
<rect class="bar bar-accent" x="230" y="60" width="34" height="80" rx="4" />
</svg>
</div>
<ul class="legend">
<li><span class="swatch"></span>Organic</li>
<li><span class="swatch"></span>Paid</li>
<li><span class="swatch swatch-accent"></span>Referral</li>
</ul>
</article>
<!-- Retention cohort -->
<article class="card" data-blur>
<header class="card-head">
<h2 class="card-title">Retention</h2>
<span class="card-tag">Cohort · Wk 1–4</span>
</header>
<div class="chart" role="img" aria-label="Bar chart of weekly retention">
<svg viewBox="0 0 280 140" width="100%" height="140" preserveAspectRatio="none">
<line class="grid-line" x1="0" y1="35" x2="280" y2="35" />
<line class="grid-line" x1="0" y1="70" x2="280" y2="70" />
<line class="grid-line" x1="0" y1="105" x2="280" y2="105" />
<rect class="bar bar-accent" x="22" y="20" width="46" height="120" rx="4" />
<rect class="bar bar-accent" x="86" y="48" width="46" height="92" rx="4" />
<rect class="bar bar-accent" x="150" y="70" width="46" height="70" rx="4" />
<rect class="bar bar-accent" x="214" y="86" width="46" height="54" rx="4" />
</svg>
</div>
<ul class="legend">
<li><span class="swatch swatch-accent"></span>Returning users by week</li>
</ul>
</article>
<!-- Top funnels table -->
<article class="card card-wide" data-blur>
<header class="card-head">
<h2 class="card-title">Top converting funnels</h2>
<span class="card-tag">By revenue</span>
</header>
<table class="data-table">
<thead>
<tr><th>Funnel</th><th>Sessions</th><th>Conv.</th><th>Revenue</th></tr>
</thead>
<tbody>
<tr><td>Pricing → Checkout</td><td>9,204</td><td>5.1%</td><td>$41,090</td></tr>
<tr><td>Trial → Pro upgrade</td><td>3,118</td><td>11.2%</td><td>$28,640</td></tr>
<tr><td>Blog → Signup</td><td>14,870</td><td>2.4%</td><td>$15,300</td></tr>
<tr><td>Referral → Checkout</td><td>2,041</td><td>7.8%</td><td>$7,450</td></tr>
</tbody>
</table>
</article>
</div>
<!-- Lock overlay -->
<div class="lock-overlay" id="lockOverlay" role="group" aria-label="Locked premium content">
<div class="lock-card">
<span class="lock-badge" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="4" y="11" width="16" height="9" rx="2" />
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
</svg>
</span>
<h2 class="lock-title">3 premium sections locked</h2>
<p class="lock-copy">Unlock revenue, retention, and funnel analytics — plus weekly
exports — with <strong>Northwind Pro</strong>.</p>
<button class="btn btn-primary btn-lg" id="unlockBtn" type="button">Unlock with Pro</button>
<button class="btn btn-link" id="plansBtn" type="button">Compare plans</button>
</div>
</div>
</section>
<!-- Plans modal -->
<div class="overlay" id="overlay" hidden></div>
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
<div class="modal-head">
<div>
<h2 class="modal-title" id="modalTitle">Upgrade your plan</h2>
<p class="modal-sub">Unlock the full report and remove every blur — instantly.</p>
</div>
<button class="icon-btn" id="modalClose" type="button" aria-label="Close dialog">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 6 12 12M18 6 6 18" />
</svg>
</button>
</div>
<div class="cycle" role="group" aria-label="Billing cycle">
<button class="cycle-opt is-active" data-cycle="monthly" type="button" aria-pressed="true">Monthly</button>
<button class="cycle-opt" data-cycle="annual" type="button" aria-pressed="false">
Annual <span class="cycle-save">Save 20%</span>
</button>
</div>
<div class="plans">
<article class="plan">
<h3 class="plan-name">Starter</h3>
<p class="plan-desc">For solo dashboards.</p>
<p class="plan-price"><span class="amount" data-monthly="$0" data-annual="$0">$0</span><span class="per" data-per>/mo</span></p>
<ul class="features">
<li>Traffic metrics</li>
<li>7-day history</li>
<li>1 project</li>
</ul>
<button class="btn btn-outline plan-cta" data-plan="Starter" type="button">Current plan</button>
</article>
<article class="plan plan-popular">
<span class="plan-badge">Most popular</span>
<h3 class="plan-name">Pro</h3>
<p class="plan-desc">The full report, unlocked.</p>
<p class="plan-price"><span class="amount" data-monthly="$24" data-annual="$19">$24</span><span class="per" data-per>/mo</span></p>
<ul class="features">
<li>Everything in Starter</li>
<li>Revenue & retention analytics</li>
<li>Funnel & cohort reports</li>
<li>Weekly CSV exports</li>
</ul>
<button class="btn btn-primary plan-cta" data-plan="Pro" data-unlock type="button">Choose Pro</button>
</article>
<article class="plan">
<h3 class="plan-name">Scale</h3>
<p class="plan-desc">For growing teams.</p>
<p class="plan-price"><span class="amount" data-monthly="$69" data-annual="$55">$69</span><span class="per" data-per>/mo</span></p>
<ul class="features">
<li>Everything in Pro</li>
<li>Up to 10 seats</li>
<li>API & webhooks</li>
<li>Priority support</li>
</ul>
<button class="btn btn-outline plan-cta" data-plan="Scale" data-unlock type="button">Choose Scale</button>
</article>
</div>
<p class="modal-foot">Prices in USD · billed <span id="cycleNote">monthly</span> · cancel anytime.</p>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
</div>
<script src="script.js"></script>
</body>
</html>Blur-locked premium content
A fictional Northwind Analytics weekly report that previews the upsell pattern used by dashboards and reporting tools: the free Visitors metric stays sharp, while Revenue and Conversion tiles show masked digits and the three premium cards — a revenue-by-channel bar chart, a retention cohort chart, and a top-funnels table, all drawn as inline SVG and HTML — sit behind a filter: blur() frosted layer. A centered lock card explains exactly what is hidden and floats above the blur with a soft shadow.
Clicking Unlock with Pro flips an is-unlocked class that animates the blur to zero, fades the overlay out, and swaps every masked value ($••,•••, •.••%) for its real figure via data-real attributes. A toast confirms the reveal, the top-bar plan pill turns Pro, and the unlock button settles into a disabled Unlocked state. The whole transition is a single CSS class flip, so it stays smooth and respects prefers-reduced-motion.
Compare plans opens an accessible dialog (role="dialog", aria-modal, focus trap, Esc-to-close, backdrop click) with Starter, Pro, and Scale tiers, a highlighted Most popular plan, and a monthly/annual segmented toggle that rewrites the prices in place. Choosing Pro or Scale closes the modal and unlocks the report too. Everything is vanilla JS with a small toast() helper, and the layout collapses to a single column down to 360px.
Illustrative UI only — fictional brand, plans, and analytics data. Unlocking is a front-end demo and does not process payments.