Nonprofit — Story Spotlight
A warm, long-form beneficiary spotlight page for a fictional water charity. Features a photo hero, magazine-style narrative with drop cap and pull quotes, a draggable before-and-after comparison, a four-image gallery with keyboard-navigable lightbox, related-story cards, and a sticky donate rail with a live fundraising thermometer, preset amounts, animated impact counters, recent-donor feed, trust badges, and native share with a fallback sheet.
MCP
Code
:root {
--brand: #1f7a6d;
--brand-d: #155e54;
--accent: #e8743b;
--accent-d: #cc5d28;
--ink: #2a2722;
--ink-2: #524d44;
--muted: #7a7368;
--bg: #faf6f0;
--surface: #ffffff;
--line: rgba(42, 39, 34, 0.1);
--line-2: rgba(42, 39, 34, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow: 0 2px 8px rgba(42, 39, 34, 0.06), 0 12px 30px rgba(42, 39, 34, 0.08);
--shadow-lg: 0 18px 50px rgba(42, 39, 34, 0.16);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
color: var(--ink);
background: var(--bg);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1, h2, h3 {
font-family: "Fraunces", Georgia, serif;
line-height: 1.12;
margin: 0;
font-weight: 600;
}
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
a { color: inherit; }
/* ---------- buttons ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
font: inherit; font-weight: 600; cursor: pointer;
border: 1px solid transparent; border-radius: 999px;
padding: 11px 20px; text-decoration: none;
transition: transform .15s ease, box-shadow .2s ease, background .2s ease;
}
.btn:active { transform: translateY(1px) scale(.99); }
.btn:focus-visible { outline: 3px solid color-mix(in srgb, var(--brand) 55%, white); outline-offset: 2px; }
.btn--accent { background: var(--accent); color: #fff; box-shadow: 0 8px 20px rgba(232, 116, 59, .28); }
.btn--accent:hover { background: var(--accent-d); transform: translateY(-1px); }
.btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--line-2); }
.btn--ghost:hover { border-color: var(--brand); color: var(--brand-d); }
.btn--block { width: 100%; }
/* ---------- reading progress ---------- */
.readbar {
position: fixed; inset: 0 0 auto 0; height: 4px; z-index: 60;
background: transparent;
}
.readbar__fill {
display: block; height: 100%; width: 0;
background: linear-gradient(90deg, var(--brand), var(--accent));
box-shadow: 0 0 12px rgba(232, 116, 59, .4);
}
/* ---------- top nav ---------- */
.topnav {
position: sticky; top: 0; z-index: 50;
display: flex; align-items: center; gap: 18px;
padding: 12px clamp(16px, 4vw, 40px);
background: color-mix(in srgb, var(--bg) 86%, transparent);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.brand { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; font-weight: 700; }
.brand__mark {
width: 30px; height: 30px; display: grid; place-items: center;
background: var(--brand); color: #fff; border-radius: 9px; font-size: 18px;
}
.brand__name { font-family: "Fraunces", serif; font-size: 18px; }
.brand__name em { color: var(--brand); font-style: normal; }
.topnav__links { display: flex; gap: 22px; margin-left: auto; font-size: 14px; font-weight: 500; }
.topnav__links a { text-decoration: none; color: var(--ink-2); transition: color .15s; }
.topnav__links a:hover { color: var(--brand-d); }
.topnav__cta { margin-left: 6px; padding: 9px 18px; }
/* ---------- hero ---------- */
.hero {
display: grid; grid-template-columns: 1.15fr 1fr;
gap: clamp(20px, 3vw, 36px);
max-width: 1160px; margin: clamp(20px, 4vw, 44px) auto 0;
padding: 0 clamp(16px, 4vw, 40px);
}
.hero__media {
position: relative; min-height: 440px; border-radius: var(--r-lg);
background:
radial-gradient(120% 90% at 20% 10%, rgba(255,255,255,.28), transparent 55%),
linear-gradient(150deg, #2a8f7f 0%, #1f7a6d 38%, #155e54 100%);
box-shadow: var(--shadow);
overflow: hidden;
}
.hero__media::after {
content: ""; position: absolute; inset: 0;
background:
radial-gradient(60% 70% at 72% 78%, rgba(232,116,59,.55), transparent 60%),
radial-gradient(40% 50% at 30% 85%, rgba(0,0,0,.22), transparent 70%);
mix-blend-mode: multiply;
}
.hero__photo-tag {
position: absolute; left: 16px; bottom: 16px; z-index: 2;
font-size: 12px; font-weight: 600; color: #fff;
background: rgba(0,0,0,.32); padding: 6px 12px; border-radius: 999px;
backdrop-filter: blur(4px);
}
.hero__panel { align-self: center; }
.eyebrow {
display: inline-block; font-size: 12px; font-weight: 700; letter-spacing: .08em;
text-transform: uppercase; color: var(--accent-d);
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
padding: 6px 12px; border-radius: 999px;
}
.hero__title { font-size: clamp(30px, 4.4vw, 50px); margin: 16px 0 12px; letter-spacing: -.01em; }
.hero__lede { font-size: 18px; color: var(--ink-2); margin: 0 0 18px; max-width: 46ch; }
.hero__meta { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 20px; }
.chip {
display: inline-flex; align-items: center; gap: 7px;
font-size: 13px; font-weight: 600; color: var(--brand-d);
background: color-mix(in srgb, var(--brand) 12%, var(--surface));
padding: 5px 12px; border-radius: 999px;
}
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 0 rgba(47,158,111,.6); animation: pulse 2s infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(47,158,111,.5);} 70% { box-shadow: 0 0 0 8px rgba(47,158,111,0);} 100% { box-shadow: 0 0 0 0 rgba(47,158,111,0);} }
.byline { font-size: 14px; color: var(--muted); }
.hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }
/* ---------- layout ---------- */
.layout {
display: grid; grid-template-columns: minmax(0, 1fr) 340px;
gap: clamp(24px, 4vw, 56px);
max-width: 1160px; margin: clamp(34px, 6vw, 64px) auto;
padding: 0 clamp(16px, 4vw, 40px);
align-items: start;
}
/* ---------- story ---------- */
.story { font-size: 18px; color: var(--ink); }
.story p { margin: 0 0 22px; max-width: 64ch; }
.dropcap::first-letter {
font-family: "Fraunces", serif; font-weight: 700; float: left;
font-size: 3.6em; line-height: .82; padding: 6px 12px 0 0; color: var(--brand);
}
.pullquote {
margin: 30px 0; padding: 22px 26px;
border-left: 4px solid var(--accent);
background: var(--surface); border-radius: var(--r-md);
box-shadow: var(--shadow);
}
.pullquote--alt { border-left-color: var(--brand); }
.pullquote blockquote {
margin: 0; font-family: "Fraunces", serif; font-style: italic;
font-size: clamp(20px, 2.6vw, 26px); line-height: 1.32; color: var(--ink);
}
.pullquote figcaption { margin-top: 10px; font-size: 14px; font-weight: 600; color: var(--muted); }
/* before / after */
.ba { margin: 34px 0; }
.ba__head h3 { font-size: 22px; }
.ba__head p { font-size: 14px; color: var(--muted); margin: 4px 0 14px; }
.ba__frame {
position: relative; height: 320px; border-radius: var(--r-lg);
overflow: hidden; box-shadow: var(--shadow); user-select: none;
touch-action: none;
}
.ba__layer { position: absolute; inset: 0; }
.ba__before {
background: linear-gradient(135deg, #8a6a45 0%, #6b4f30 60%, #4a3621 100%);
}
.ba__before::after, .ba__after::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(80% 100% at 50% 120%, rgba(0,0,0,.35), transparent 60%);
}
.ba__after {
background: linear-gradient(135deg, #2a8f7f 0%, #1f7a6d 55%, #155e54 100%);
clip-path: inset(0 0 0 50%);
}
.ba__label {
position: absolute; bottom: 14px; left: 14px; z-index: 3;
font-size: 13px; font-weight: 700; color: #fff;
background: rgba(0,0,0,.34); padding: 6px 12px; border-radius: 999px;
}
.ba__label--right { left: auto; right: 14px; }
.ba__range {
position: absolute; inset: 0; width: 100%; height: 100%;
margin: 0; opacity: 0; cursor: ew-resize; z-index: 5;
}
.ba__handle {
position: absolute; top: 50%; left: 50%; z-index: 4;
transform: translate(-50%, -50%);
width: 44px; height: 44px; border-radius: 50%;
display: grid; place-items: center; color: var(--brand-d); font-size: 18px;
background: #fff; box-shadow: 0 4px 14px rgba(0,0,0,.3);
}
.ba__handle::before, .ba__handle::after {
content: ""; position: absolute; top: -1000px; bottom: -1000px;
width: 3px; background: rgba(255,255,255,.85);
}
.ba__handle::before { right: 50%; }
.ba__handle::after { left: 50%; }
/* gallery */
.gallery { margin: 36px 0; }
.gallery__h { font-size: 22px; margin-bottom: 14px; }
.gallery__grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
}
.shot {
position: relative; border: none; cursor: pointer; padding: 0;
aspect-ratio: 1; border-radius: var(--r-md); overflow: hidden;
box-shadow: var(--shadow); transition: transform .18s ease, box-shadow .2s ease;
}
.shot:hover { transform: translateY(-3px) scale(1.02); box-shadow: var(--shadow-lg); }
.shot:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }
.shot--1 { background: linear-gradient(150deg, #3a93c9, #1f6aa0); }
.shot--2 { background: linear-gradient(150deg, #2a8f7f, #155e54); }
.shot--3 { background: linear-gradient(150deg, #e8a14b, #cc5d28); }
.shot--4 { background: linear-gradient(150deg, #9a6fc0, #6b4690); }
.shot__cap {
position: absolute; left: 8px; bottom: 8px; right: 8px;
font-size: 12px; font-weight: 700; color: #fff; text-align: left;
text-shadow: 0 1px 4px rgba(0,0,0,.5);
}
/* inline share */
.share-inline {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
margin: 30px 0 0; padding-top: 22px; border-top: 1px solid var(--line);
font-size: 15px; font-weight: 600; color: var(--ink-2);
}
.ishare {
font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
background: var(--surface); border: 1px solid var(--line-2);
color: var(--ink); padding: 8px 14px; border-radius: 999px;
transition: all .15s ease;
}
.ishare:hover { border-color: var(--brand); color: var(--brand-d); transform: translateY(-1px); }
/* ---------- rail ---------- */
.rail { position: sticky; top: 78px; display: grid; gap: 18px; }
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: 22px; box-shadow: var(--shadow);
}
.give__eyebrow { font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--accent-d); }
.give__title { font-size: 22px; margin: 4px 0 16px; }
.thermo {
height: 14px; border-radius: 999px; overflow: hidden;
background: color-mix(in srgb, var(--brand) 12%, var(--bg));
}
.thermo__fill {
display: block; height: 100%;
background: linear-gradient(90deg, var(--brand), #2a8f7f);
border-radius: 999px; transition: width .6s cubic-bezier(.22,1,.36,1);
}
.thermo__nums { display: flex; align-items: baseline; gap: 7px; margin: 10px 0 2px; }
.thermo__nums strong { font-family: "Fraunces", serif; font-size: 26px; color: var(--brand-d); }
.thermo__nums span { font-size: 14px; color: var(--muted); }
.give__people { font-size: 13px; color: var(--muted); margin: 0 0 16px; }
.amounts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.amt {
font: inherit; font-weight: 700; cursor: pointer;
padding: 11px 4px; border-radius: var(--r-sm);
background: var(--bg); border: 1.5px solid var(--line-2); color: var(--ink);
transition: all .15s ease;
}
.amt:hover { border-color: var(--brand); }
.amt.is-active { background: var(--brand); color: #fff; border-color: var(--brand); box-shadow: 0 6px 14px rgba(31,122,109,.3); }
.amt--custom { font-size: 13px; }
.give__hint { font-size: 13px; color: var(--ink-2); margin: 12px 0 14px; min-height: 2.6em; }
.trust { list-style: none; margin: 18px 0 0; padding: 16px 0 0; border-top: 1px solid var(--line); display: grid; gap: 9px; }
.trust li { display: flex; align-items: flex-start; gap: 9px; font-size: 13px; color: var(--ink-2); }
.trust li span { color: var(--ok); font-weight: 700; }
/* donors */
.donors__h { font-size: 17px; margin-bottom: 14px; }
.donors__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
.donors__list li { display: flex; align-items: center; gap: 12px; }
.av {
width: 38px; height: 38px; flex: 0 0 38px; border-radius: 50%;
display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff;
background: linear-gradient(140deg, var(--brand), var(--accent));
}
.donors__list b { font-size: 14px; display: block; }
.donors__list small { font-size: 12px; color: var(--muted); }
.donors__list li.is-new { animation: fadein .5s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
/* ---------- stats ---------- */
.stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px;
max-width: 1160px; margin: 0 auto; padding: 30px clamp(16px, 4vw, 40px);
}
.stat {
text-align: center; padding: 24px 14px; border-radius: var(--r-lg);
background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow);
}
.stat b { display: block; font-family: "Fraunces", serif; font-size: clamp(26px, 3.6vw, 38px); color: var(--brand-d); }
.stat span { font-size: 13px; color: var(--muted); }
/* ---------- related ---------- */
.related { max-width: 1160px; margin: 30px auto 60px; padding: 0 clamp(16px, 4vw, 40px); }
.related__h { font-size: clamp(24px, 3.4vw, 32px); margin-bottom: 20px; }
.related__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.rcard {
display: flex; flex-direction: column; gap: 8px;
text-decoration: none; color: #fff; padding: 22px;
border-radius: var(--r-lg); min-height: 210px; position: relative;
box-shadow: var(--shadow); overflow: hidden;
transition: transform .2s ease, box-shadow .2s ease;
}
.rcard:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.rcard::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent, rgba(0,0,0,.28)); z-index: 0; }
.rcard > * { position: relative; z-index: 1; }
.rcard--a { background: linear-gradient(150deg, #3a93c9, #1f6aa0); }
.rcard--b { background: linear-gradient(150deg, #d4503e, #a23528); }
.rcard--c { background: linear-gradient(150deg, #2a8f7f, #155e54); }
.rcard__tag { font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; background: rgba(255,255,255,.22); padding: 4px 10px; border-radius: 999px; align-self: flex-start; }
.rcard h3 { font-size: 20px; margin-top: auto; }
.rcard p { font-size: 14px; margin: 0; opacity: .92; }
.rcard__more { font-size: 13px; font-weight: 700; margin-top: 6px; }
/* ---------- footer ---------- */
.foot {
text-align: center; padding: 36px 16px; border-top: 1px solid var(--line);
background: var(--surface);
}
.foot p { margin: 4px 0; font-size: 13px; color: var(--muted); }
.foot strong { color: var(--ink); }
/* ---------- lightbox ---------- */
.lightbox {
position: fixed; inset: 0; z-index: 90; display: none;
align-items: center; justify-content: center; gap: 12px;
background: rgba(20, 17, 13, .82); backdrop-filter: blur(6px);
padding: 20px;
}
.lightbox.is-open { display: flex; animation: fadein .25s ease; }
.lightbox__inner { margin: 0; max-width: 760px; width: 100%; }
.lightbox__img {
width: 100%; aspect-ratio: 16/10; border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
}
.lightbox__inner figcaption { color: #fff; text-align: center; margin-top: 14px; font-size: 15px; }
.lightbox__close {
position: absolute; top: 18px; right: 18px;
width: 42px; height: 42px; border-radius: 50%; border: none; cursor: pointer;
background: rgba(255,255,255,.16); color: #fff; font-size: 18px;
}
.lightbox__close:hover { background: rgba(255,255,255,.3); }
.lightbox__nav {
width: 46px; height: 46px; border-radius: 50%; border: none; cursor: pointer;
background: rgba(255,255,255,.16); color: #fff; font-size: 26px; flex: 0 0 auto;
}
.lightbox__nav:hover { background: rgba(255,255,255,.3); }
/* ---------- share sheet ---------- */
.sheet {
position: fixed; inset: 0; z-index: 95; display: none;
align-items: flex-end; justify-content: center;
background: rgba(20, 17, 13, .5);
}
.sheet.is-open { display: flex; animation: fadein .2s ease; }
.sheet__panel {
background: var(--surface); width: 100%; max-width: 440px;
border-radius: var(--r-lg) var(--r-lg) 0 0; padding: 24px;
box-shadow: var(--shadow-lg); animation: slideup .3s cubic-bezier(.22,1,.36,1);
}
@keyframes slideup { from { transform: translateY(100%); } to { transform: none; } }
.sheet__panel h3 { font-size: 20px; margin-bottom: 16px; }
.sheet__row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px);
background: var(--ink); color: #fff; font-size: 14px; font-weight: 500;
padding: 12px 20px; border-radius: 999px; z-index: 120;
box-shadow: var(--shadow-lg); opacity: 0; pointer-events: none;
transition: opacity .25s ease, transform .25s ease;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- responsive ---------- */
@media (max-width: 920px) {
.hero { grid-template-columns: 1fr; }
.hero__media { min-height: 300px; }
.layout { grid-template-columns: 1fr; }
.rail { position: static; }
.related__grid { grid-template-columns: 1fr 1fr; }
.stats { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 520px) {
.topnav__links { display: none; }
.gallery__grid { grid-template-columns: 1fr 1fr; }
.related__grid { grid-template-columns: 1fr; }
.stats { grid-template-columns: 1fr 1fr; gap: 12px; }
.story { font-size: 17px; }
.hero__title { font-size: 28px; }
.amounts { grid-template-columns: repeat(2, 1fr); }
.ba__frame { height: 250px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; scroll-behavior: auto; transition: none !important; }
}(function () {
"use strict";
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2400);
}
/* ---------- reading progress ---------- */
var readFill = document.getElementById("readFill");
var readbar = document.querySelector(".readbar");
function updateProgress() {
var h = document.documentElement;
var scrollable = h.scrollHeight - h.clientHeight;
var pct = scrollable > 0 ? (h.scrollTop / scrollable) * 100 : 0;
pct = Math.max(0, Math.min(100, pct));
readFill.style.width = pct + "%";
readbar.setAttribute("aria-valuenow", Math.round(pct));
}
window.addEventListener("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress);
updateProgress();
/* ---------- before / after slider ---------- */
var baRange = document.getElementById("baRange");
var baAfter = document.getElementById("baAfter");
var baHandle = document.getElementById("baHandle");
function setBa(v) {
baAfter.style.clipPath = "inset(0 0 0 " + v + "%)";
baHandle.style.left = v + "%";
}
if (baRange) {
baRange.addEventListener("input", function () {
setBa(this.value);
});
setBa(baRange.value);
}
/* ---------- gallery + lightbox ---------- */
var shots = Array.prototype.slice.call(document.querySelectorAll(".shot"));
var lightbox = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbCap = document.getElementById("lbCap");
var lbClose = document.getElementById("lbClose");
var lbPrev = document.getElementById("lbPrev");
var lbNext = document.getElementById("lbNext");
var current = 0;
var gradients = [
"linear-gradient(150deg, #3a93c9, #1f6aa0)",
"linear-gradient(150deg, #2a8f7f, #155e54)",
"linear-gradient(150deg, #e8a14b, #cc5d28)",
"linear-gradient(150deg, #9a6fc0, #6b4690)"
];
function showShot(i) {
current = (i + shots.length) % shots.length;
lbImg.style.background = gradients[current];
lbCap.textContent = shots[current].getAttribute("data-cap");
lbImg.setAttribute("aria-label", shots[current].getAttribute("data-cap"));
}
function openLightbox(i) {
showShot(i);
lightbox.classList.add("is-open");
lightbox.setAttribute("aria-hidden", "false");
lbClose.focus();
}
function closeLightbox() {
lightbox.classList.remove("is-open");
lightbox.setAttribute("aria-hidden", "true");
if (shots[current]) shots[current].focus();
}
shots.forEach(function (s, i) {
s.addEventListener("click", function () {
openLightbox(i);
});
});
if (lbClose) lbClose.addEventListener("click", closeLightbox);
if (lbPrev) lbPrev.addEventListener("click", function () { showShot(current - 1); });
if (lbNext) lbNext.addEventListener("click", function () { showShot(current + 1); });
if (lightbox) {
lightbox.addEventListener("click", function (e) {
if (e.target === lightbox) closeLightbox();
});
}
document.addEventListener("keydown", function (e) {
if (!lightbox.classList.contains("is-open")) return;
if (e.key === "Escape") closeLightbox();
else if (e.key === "ArrowLeft") showShot(current - 1);
else if (e.key === "ArrowRight") showShot(current + 1);
});
/* ---------- share ---------- */
var shareSheet = document.getElementById("shareSheet");
var shareBtn = document.getElementById("shareBtn");
var sheetClose = document.getElementById("sheetClose");
var storyUrl = "https://brightwells.example/stories/amara";
var storyTitle = "Amara walked 6 hours for water. Now it's 6 minutes.";
function openSheet() {
if (navigator.share) {
navigator.share({ title: storyTitle, url: storyUrl }).catch(function () {});
return;
}
shareSheet.classList.add("is-open");
shareSheet.setAttribute("aria-hidden", "false");
}
function closeSheet() {
shareSheet.classList.remove("is-open");
shareSheet.setAttribute("aria-hidden", "true");
}
if (shareBtn) shareBtn.addEventListener("click", openSheet);
if (sheetClose) sheetClose.addEventListener("click", closeSheet);
if (shareSheet) {
shareSheet.addEventListener("click", function (e) {
if (e.target === shareSheet) closeSheet();
});
}
function handleShare(net) {
if (net === "link") {
if (navigator.clipboard) {
navigator.clipboard.writeText(storyUrl).then(function () {
toast("Link copied to clipboard");
}, function () {
toast("Link: " + storyUrl);
});
} else {
toast("Link: " + storyUrl);
}
} else if (net === "x") {
toast("Opening a draft post…");
} else if (net === "wa") {
toast("Opening WhatsApp…");
} else if (net === "mail") {
toast("Opening your email app…");
}
closeSheet();
}
document.querySelectorAll(".ishare").forEach(function (b) {
b.addEventListener("click", function () {
handleShare(this.getAttribute("data-net"));
});
});
/* ---------- donate amounts ---------- */
var amts = Array.prototype.slice.call(document.querySelectorAll(".amt"));
var giveHint = document.getElementById("giveHint");
var donateAmt = document.getElementById("donateAmt");
var donateBtn = document.getElementById("donateBtn");
var selected = 60;
var hints = {
25: "$25 keeps a single tap running for a month.",
60: "$60 funds a metre of new pipe to the school.",
120: "$120 trains a village committee member on upkeep.",
custom: "Every amount is pooled toward Tank II."
};
function selectAmt(btn) {
amts.forEach(function (a) { a.classList.remove("is-active"); });
btn.classList.add("is-active");
var val = btn.getAttribute("data-amt");
if (val === "custom") {
var raw = window.prompt("Enter a custom amount in USD:", "75");
var n = parseInt((raw || "").replace(/[^0-9]/g, ""), 10);
if (!n || n < 1) {
selectAmt(document.querySelector('.amt[data-amt="60"]'));
return;
}
selected = n;
giveHint.textContent = "Thank you — your $" + n + " goes straight to the field.";
donateAmt.textContent = "$" + n;
btn.textContent = "$" + n;
} else {
selected = parseInt(val, 10);
giveHint.textContent = hints[selected] || hints.custom;
donateAmt.textContent = "$" + selected;
}
}
amts.forEach(function (a) {
a.addEventListener("click", function () { selectAmt(this); });
});
/* ---------- donate flow + live thermometer ---------- */
var thermoFill = document.getElementById("thermoFill");
var raisedEl = document.getElementById("raised");
var backersEl = document.getElementById("backers");
var donorList = document.getElementById("donorList");
var raised = 13640;
var goal = 20000;
var backers = 284;
var firstNames = ["Aïsha", "Marco", "Lena", "Kwame", "Sofía", "Yusuf", "Mei", "Tomás", "Priya"];
function refreshThermo() {
var pct = Math.min(100, (raised / goal) * 100);
thermoFill.style.width = pct.toFixed(1) + "%";
raisedEl.textContent = "$" + raised.toLocaleString("en-US");
backersEl.textContent = backers;
}
function addDonor(name, amt, note) {
var li = document.createElement("li");
li.className = "is-new";
var initials = name.split(/\s+/).map(function (w) { return w[0]; }).join("").slice(0, 2).toUpperCase();
li.innerHTML =
'<span class="av" aria-hidden="true">' + initials + "</span>" +
"<div><b>" + name + "</b><small>$" + amt + (note ? " · " + note : "") + "</small></div>";
donorList.insertBefore(li, donorList.firstChild);
while (donorList.children.length > 5) {
donorList.removeChild(donorList.lastChild);
}
}
if (donateBtn) {
donateBtn.addEventListener("click", function () {
raised += selected;
backers += 1;
refreshThermo();
addDonor("You", selected, "thank you!");
toast("Thank you! $" + selected + " toward Tank II 💧");
});
}
// simulate other donors arriving while you read
setInterval(function () {
if (Math.random() > 0.55) {
var amt = [25, 30, 60, 100][Math.floor(Math.random() * 4)];
var name = firstNames[Math.floor(Math.random() * firstNames.length)];
raised += amt;
backers += 1;
refreshThermo();
addDonor(name + " " + String.fromCharCode(65 + Math.floor(Math.random() * 26)) + ".", amt, "just now");
}
}, 9000);
refreshThermo();
/* ---------- count-up stats ---------- */
var counted = false;
function runCounts() {
if (counted) return;
counted = true;
document.querySelectorAll(".stat b[data-count]").forEach(function (el) {
var target = parseInt(el.getAttribute("data-count"), 10);
var suffix = el.getAttribute("data-suffix") || "";
var start = null;
var dur = 1400;
function step(ts) {
if (start === null) start = ts;
var p = Math.min(1, (ts - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = Math.round(target * eased);
el.textContent = val.toLocaleString("en-US") + suffix;
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
var statsSection = document.querySelector(".stats");
if ("IntersectionObserver" in window && statsSection) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) runCounts(); });
}, { threshold: 0.4 });
io.observe(statsSection);
} else {
runCounts();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Amara's Path — Bright Wells Foundation</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=Fraunces:ital,opsz,wght@0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Reading progress -->
<div class="readbar" role="progressbar" aria-label="Reading progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<span class="readbar__fill" id="readFill"></span>
</div>
<header class="topnav">
<a class="brand" href="#top" aria-label="Bright Wells Foundation home">
<span class="brand__mark" aria-hidden="true">◍</span>
<span class="brand__name">Bright Wells <em>Foundation</em></span>
</a>
<nav class="topnav__links" aria-label="Primary">
<a href="#story">Story</a>
<a href="#gallery">Gallery</a>
<a href="#impact">Impact</a>
<a href="#related">More stories</a>
</nav>
<a class="btn btn--accent topnav__cta" href="#give">Donate</a>
</header>
<main id="top">
<!-- HERO -->
<section class="hero" id="story">
<div class="hero__media" role="img" aria-label="Amara stands beside a newly built community well, smiling with neighbours">
<span class="hero__photo-tag">Photo · Kano region, 2026</span>
</div>
<div class="hero__panel">
<span class="eyebrow">Beneficiary Spotlight · Clean Water</span>
<h1 class="hero__title">Amara walked 6 hours for water.<br />Now it's 6 minutes.</h1>
<p class="hero__lede">One well changed a morning routine — and the whole arc of a girl's education. This is her story, in her village's words.</p>
<div class="hero__meta">
<span class="chip"><span class="dot" aria-hidden="true"></span>Live campaign</span>
<span class="byline">By <strong>Naïma Okafor</strong> · 7 min read</span>
</div>
<div class="hero__actions">
<a class="btn btn--accent" href="#give">Help the next village</a>
<button class="btn btn--ghost" id="shareBtn" type="button" aria-haspopup="true">
<span aria-hidden="true">↗</span> Share story
</button>
</div>
</div>
</section>
<div class="layout">
<article class="story" aria-labelledby="story-h">
<h2 id="story-h" class="sr-only">Amara's story</h2>
<p class="dropcap">Before the well, the day began at four in the morning. Amara Bello, then eleven, would lift an empty jerrican onto her head and join the women on the long road toward the river. By the time the sun was high, she had walked the equivalent of a half-marathon — and missed the first lessons of the school day entirely.</p>
<p>The water she carried back was rarely clean. In the dry months the river thinned to a brown trickle shared with livestock, and the clinic in the next town counted child after child with the same preventable illnesses. For families in Gidan Sabo, water was not a tap. It was a tax paid in hours, in health, and too often in futures.</p>
<figure class="pullquote">
<blockquote>"I thought walking for water was just what girls did. I did not know there was another way to live."</blockquote>
<figcaption>— Amara Bello, now 14</figcaption>
</figure>
<p>That changed in the spring of 2026, when a Bright Wells survey team mapped the village's groundwater and found a viable aquifer barely 40 metres below the schoolyard. With a community committee already in place, the build moved fast: a solar-powered borehole, a storage tank, and four shared taps within a short walk of every home.</p>
<!-- BEFORE / AFTER -->
<div class="ba" aria-label="Before and after the well was built">
<div class="ba__head">
<h3>Before & after, one well</h3>
<p>Drag the handle to compare the same morning, one year apart.</p>
</div>
<div class="ba__frame" id="baFrame">
<div class="ba__layer ba__before" role="img" aria-label="Before: a long dusty walk to a distant river">
<span class="ba__label">Before · 6 hr round trip</span>
</div>
<div class="ba__layer ba__after" id="baAfter" role="img" aria-label="After: a short walk to a clean community tap">
<span class="ba__label ba__label--right">After · 6 min walk</span>
</div>
<input class="ba__range" id="baRange" type="range" min="0" max="100" value="50" aria-label="Compare before and after" />
<span class="ba__handle" id="baHandle" aria-hidden="true">⇆</span>
</div>
</div>
<p>The difference was immediate and measurable. Amara's school attendance climbed from barely half her days to nearly every one. She is now top of her science class and talks, unprompted, about becoming a hydrologist — someone who finds water for other villages the way Bright Wells found it for hers.</p>
<figure class="pullquote pullquote--alt">
<blockquote>"The well did not just give us water. It gave our daughters their mornings back."</blockquote>
<figcaption>— Hadiza Bello, Amara's mother & well committee treasurer</figcaption>
</figure>
<p>Gidan Sabo's committee now collects a small maintenance fee from each household, banked transparently and audited twice a year. It is a model designed to outlast the donors who started it: when the next pump part wears out, the village can already pay for it themselves. That is the quiet goal behind every Bright Wells project — not charity that returns each year, but infrastructure that stays.</p>
<!-- GALLERY -->
<section class="gallery" id="gallery" aria-label="Photo gallery">
<h3 class="gallery__h">From the field</h3>
<div class="gallery__grid" id="galleryGrid">
<button class="shot shot--1" type="button" data-cap="The borehole strike — water at 38 metres." aria-label="View photo: water strike at the borehole"><span class="shot__cap">The strike, 38m</span></button>
<button class="shot shot--2" type="button" data-cap="Amara fills a jerrican in under a minute at the new tap." aria-label="View photo: Amara at the new tap"><span class="shot__cap">First tap</span></button>
<button class="shot shot--3" type="button" data-cap="The well committee signs the maintenance ledger." aria-label="View photo: well committee meeting"><span class="shot__cap">The committee</span></button>
<button class="shot shot--4" type="button" data-cap="Back in class by 8am — Amara's attendance, charted." aria-label="View photo: classroom"><span class="shot__cap">Back in class</span></button>
</div>
</section>
<p>Amara's story is one of 312 wells Bright Wells has commissioned across the region — each with a name, a committee, and a ledger anyone can read. Her village's next goal is a second tank to serve the new arrivals from a drought further north. It is, as the committee likes to say, water finding its way to more people.</p>
<div class="share-inline" aria-label="Share this story">
<span>Share Amara's story:</span>
<button class="ishare" type="button" data-net="link">Copy link</button>
<button class="ishare" type="button" data-net="x">Post</button>
<button class="ishare" type="button" data-net="mail">Email</button>
</div>
</article>
<!-- STICKY DONATE / IMPACT -->
<aside class="rail" id="give" aria-label="Donate and impact">
<div class="give card">
<span class="give__eyebrow">Fund the next village</span>
<h3 class="give__title">Gidan Sabo · Tank II</h3>
<div class="thermo" aria-hidden="true">
<span class="thermo__fill" id="thermoFill" style="width:68%"></span>
</div>
<div class="thermo__nums">
<strong id="raised">$13,640</strong>
<span>raised of <b>$20,000</b></span>
</div>
<p class="give__people"><span id="backers">284</span> donors · 39 days left</p>
<div class="amounts" role="group" aria-label="Choose a donation amount">
<button class="amt" type="button" data-amt="25">$25</button>
<button class="amt is-active" type="button" data-amt="60">$60</button>
<button class="amt" type="button" data-amt="120">$120</button>
<button class="amt amt--custom" type="button" data-amt="custom">Other</button>
</div>
<p class="give__hint" id="giveHint">$60 funds a metre of new pipe to the school.</p>
<button class="btn btn--accent btn--block" id="donateBtn" type="button">Donate <span id="donateAmt">$60</span></button>
<ul class="trust">
<li><span aria-hidden="true">✓</span> Registered charity · EIN 88-0142277</li>
<li><span aria-hidden="true">✓</span> Tax-deductible receipt issued</li>
<li><span aria-hidden="true">✓</span> 91¢ of every $1 reaches the field</li>
</ul>
</div>
<div class="card donors" id="impact">
<h3 class="donors__h">Recent donors</h3>
<ul class="donors__list" id="donorList">
<li><span class="av" aria-hidden="true">TM</span><div><b>Tomás M.</b><small>$120 · "For my daughter Lucía."</small></div></li>
<li><span class="av" aria-hidden="true">KR</span><div><b>Kemi R.</b><small>$60 · monthly</small></div></li>
<li><span class="av" aria-hidden="true">JP</span><div><b>Jonas P.</b><small>$25</small></div></li>
</ul>
</div>
</aside>
</div>
<!-- IMPACT NUMBERS -->
<section class="stats" aria-label="Programme impact">
<div class="stat"><b data-count="312">0</b><span>wells commissioned</span></div>
<div class="stat"><b data-count="148000">0</b><span>people with clean water</span></div>
<div class="stat"><b data-count="6">0</b><span>hours saved each day</span></div>
<div class="stat"><b data-count="91" data-suffix="%">0</b><span>reaches the field</span></div>
</section>
<!-- RELATED STORIES -->
<section class="related" id="related" aria-label="More stories">
<h2 class="related__h">More stories of impact</h2>
<div class="related__grid">
<a class="rcard rcard--a" href="#story">
<span class="rcard__tag">Education</span>
<h3>The library built from a shipping container</h3>
<p>How 40 students in Eldoret got their first reading room.</p>
<span class="rcard__more">Read story →</span>
</a>
<a class="rcard rcard--b" href="#story">
<span class="rcard__tag">Health</span>
<h3>A midwife on a solar-charged motorbike</h3>
<p>Reaching mothers across 60km of flooded roads.</p>
<span class="rcard__more">Read story →</span>
</a>
<a class="rcard rcard--c" href="#story">
<span class="rcard__tag">Livelihoods</span>
<h3>The co-op that turned drought into mango jam</h3>
<p>17 women, one shared kitchen, a growing order book.</p>
<span class="rcard__more">Read story →</span>
</a>
</div>
</section>
</main>
<footer class="foot">
<p><strong>Bright Wells Foundation</strong> · A fictional charity for demonstration only.</p>
<p>Names, photos, and donation flows are illustrative.</p>
</footer>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Photo viewer">
<button class="lightbox__close" id="lbClose" type="button" aria-label="Close">✕</button>
<button class="lightbox__nav lightbox__nav--prev" id="lbPrev" type="button" aria-label="Previous photo">‹</button>
<figure class="lightbox__inner">
<div class="lightbox__img" id="lbImg" role="img"></div>
<figcaption id="lbCap"></figcaption>
</figure>
<button class="lightbox__nav lightbox__nav--next" id="lbNext" type="button" aria-label="Next photo">›</button>
</div>
<!-- Share sheet -->
<div class="sheet" id="shareSheet" aria-hidden="true">
<div class="sheet__panel" role="dialog" aria-modal="true" aria-label="Share">
<h3>Share this story</h3>
<div class="sheet__row">
<button class="ishare" data-net="link" type="button">Copy link</button>
<button class="ishare" data-net="x" type="button">Post</button>
<button class="ishare" data-net="wa" type="button">WhatsApp</button>
<button class="ishare" data-net="mail" type="button">Email</button>
</div>
<button class="btn btn--ghost btn--block" id="sheetClose" type="button">Close</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Story Spotlight
A complete beneficiary spotlight page for a fictional clean-water charity. A full-bleed photo hero opens Amara’s story, followed by a magazine-style narrative with a drop cap, two pull quotes, and a draggable before/after comparison block. A four-image field gallery opens into a keyboard-navigable lightbox, and a row of related stories closes the read.
The interactions are all vanilla JS. A reading-progress bar tracks scroll position, animated impact counters fire when the stats band scrolls into view, and a draggable slider reveals the before/after scenes. Sharing uses the native Web Share API with a graceful bottom-sheet fallback and copy-to-clipboard.
The sticky donate rail anchors the page: preset amounts update a contextual hint and the CTA label, a live fundraising thermometer animates as donations land, simulated donors trickle into a recent-donor feed while you read, and trust badges reinforce transparency. A small toast helper confirms every action.
Illustrative UI only — fictional organization, not a real charity or donation system.