Nonprofit — Volunteer Signup
A warm, hopeful volunteer recruitment page for a fictional charity. Browse open roles as photo cards showing cause, skills, schedule, location and a live spots-left bar that shifts amber when seats run low and red when a role is fully staffed. Chip filters narrow by cause, time commitment and on-site versus remote, with an instant text search. Clicking a card slides open a detail drawer with an apply form — validated availability, interests and contact fields — confirmed through a small toast, all self-contained vanilla JS.
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-sm: 0 1px 2px rgba(42, 39, 34, 0.06), 0 2px 8px rgba(42, 39, 34, 0.05);
--shadow-md: 0 6px 22px rgba(42, 39, 34, 0.1);
--shadow-lg: 0 18px 50px rgba(42, 39, 34, 0.18);
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--sans);
line-height: 1.6;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { font-family: var(--serif); line-height: 1.15; margin: 0; letter-spacing: -0.01em; }
a { color: inherit; }
.wrap { width: min(1140px, 100% - 40px); margin-inline: auto; }
.muted { color: var(--muted); }
.eyebrow {
display: inline-block;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--brand);
margin-bottom: 0.6rem;
}
.eyebrow.light { color: rgba(255, 255, 255, 0.85); }
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--ink);
color: #fff;
padding: 0.6rem 1rem;
border-radius: 0 0 var(--r-sm) 0;
z-index: 100;
}
.skip-link:focus { left: 0; }
/* Buttons */
.btn {
--b: var(--brand);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
font-family: var(--sans);
font-weight: 600;
font-size: 0.95rem;
padding: 0.72rem 1.25rem;
border: 1px solid transparent;
border-radius: 999px;
cursor: pointer;
text-decoration: none;
transition: transform 0.12s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.btn:active { transform: translateY(1px); }
.btn-sm { padding: 0.5rem 0.95rem; font-size: 0.88rem; }
.btn-brand { background: var(--brand); color: #fff; box-shadow: var(--shadow-sm); }
.btn-brand:hover { background: var(--brand-d); box-shadow: var(--shadow-md); }
.btn-accent { background: var(--accent); color: #fff; box-shadow: var(--shadow-sm); }
.btn-accent:hover { background: var(--accent-d); box-shadow: var(--shadow-md); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-ghost:hover { border-color: var(--ink); background: rgba(42, 39, 34, 0.03); }
.btn-block { width: 100%; }
:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* Header */
.site-header {
position: sticky;
top: 0;
z-index: 40;
background: rgba(250, 246, 240, 0.86);
backdrop-filter: saturate(150%) blur(10px);
border-bottom: 1px solid var(--line);
}
.header-inner { display: flex; align-items: center; justify-content: space-between; height: 66px; }
.brand { display: inline-flex; align-items: center; gap: 0.55rem; text-decoration: none; color: var(--ink); }
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 11px;
background: linear-gradient(140deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.brand-text { font-family: var(--serif); font-weight: 600; font-size: 1.2rem; }
.brand-text em { color: var(--accent); font-style: normal; }
.header-nav { display: flex; align-items: center; gap: 1.4rem; }
.header-nav a { text-decoration: none; font-weight: 500; font-size: 0.95rem; color: var(--ink-2); }
.header-nav a:not(.btn):hover { color: var(--ink); }
/* Hero */
.hero { padding: 3.4rem 0 2.6rem; }
.hero-inner { display: grid; grid-template-columns: 1.25fr 0.85fr; gap: 2.6rem; align-items: center; }
.hero-copy h1 { font-size: clamp(2.1rem, 4.4vw, 3.3rem); font-weight: 600; }
.lede { font-size: 1.12rem; color: var(--ink-2); margin: 1rem 0 1.5rem; max-width: 38ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-bottom: 1.5rem; }
.trust-row { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem 1.3rem; margin: 0; padding: 0; font-size: 0.86rem; color: var(--ink-2); font-weight: 500; }
.trust-row li { display: inline-flex; align-items: center; gap: 0.45rem; }
.badge-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.18); }
.hero-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.85rem;
padding: 1.5rem;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-md);
position: relative;
}
.hero-stats::before {
content: "";
position: absolute;
inset: 0;
border-radius: var(--r-lg);
background: linear-gradient(160deg, rgba(31, 122, 109, 0.07), rgba(232, 116, 59, 0.06));
pointer-events: none;
}
.stat { position: relative; }
.stat strong { display: block; font-family: var(--serif); font-size: 1.85rem; font-weight: 600; color: var(--brand-d); line-height: 1; }
.stat span { font-size: 0.82rem; color: var(--muted); }
.stat-wide { grid-column: 1 / -1; padding-top: 0.6rem; border-top: 1px solid var(--line); }
.stat-wide strong { font-size: 1.5rem; color: var(--accent-d); }
/* Section head */
.section-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1.4rem;
flex-wrap: wrap;
margin: 2.4rem 0 1.2rem;
}
.section-head h2 { font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 600; }
.search-box {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 0.55rem 1rem;
color: var(--muted);
min-width: 250px;
box-shadow: var(--shadow-sm);
}
.search-box:focus-within { border-color: var(--brand); }
.search-box input { border: 0; outline: 0; background: transparent; font: inherit; color: var(--ink); width: 100%; }
/* Filters */
.filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem 1.4rem; margin-bottom: 1.6rem; }
.filter-group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; }
.filter-label { font-size: 0.78rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); margin-right: 0.2rem; }
.chip {
font: inherit;
font-size: 0.86rem;
font-weight: 500;
padding: 0.4rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
transition: all 0.14s ease;
}
.chip:hover { border-color: var(--brand); color: var(--brand-d); }
.chip.is-active { background: var(--brand); border-color: var(--brand); color: #fff; box-shadow: var(--shadow-sm); }
.link-reset {
font: inherit;
font-size: 0.86rem;
font-weight: 600;
background: none;
border: 0;
color: var(--accent-d);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
/* Cards */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
gap: 1.2rem;
}
.card {
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.16s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--line-2); }
.card.is-highlight { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.2), var(--shadow-md); }
.card-photo {
height: 130px;
position: relative;
display: flex;
align-items: flex-end;
padding: 0.85rem;
color: #fff;
}
.card-photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 35%, rgba(0, 0, 0, 0.32)); }
.card-photo .photo-cap { position: relative; z-index: 1; font-size: 0.78rem; font-weight: 600; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); }
.cause-tag {
position: absolute;
top: 0.7rem;
left: 0.7rem;
z-index: 1;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.25rem 0.6rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--ink);
}
.remote-tag {
position: absolute;
top: 0.7rem;
right: 0.7rem;
z-index: 1;
font-size: 0.72rem;
font-weight: 700;
padding: 0.25rem 0.6rem;
border-radius: 999px;
background: var(--accent);
color: #fff;
}
.card-body { padding: 1rem 1.1rem 1.1rem; display: flex; flex-direction: column; flex: 1; gap: 0.55rem; }
.card-body h3 { font-size: 1.18rem; font-weight: 600; }
.card-org { font-size: 0.85rem; color: var(--muted); font-weight: 500; margin-top: -0.2rem; }
.card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem 0.7rem; font-size: 0.82rem; color: var(--ink-2); margin-top: 0.1rem; }
.card-meta span { display: inline-flex; align-items: center; gap: 0.3rem; }
.card-meta svg { color: var(--brand); flex: none; }
.skills { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.1rem; }
.skill-pill { font-size: 0.74rem; font-weight: 600; padding: 0.2rem 0.55rem; border-radius: var(--r-sm); background: rgba(31, 122, 109, 0.09); color: var(--brand-d); }
.spots { margin-top: auto; padding-top: 0.4rem; }
.spots-bar { height: 7px; border-radius: 999px; background: rgba(42, 39, 34, 0.1); overflow: hidden; }
.spots-bar > i { display: block; height: 100%; border-radius: 999px; background: var(--ok); transition: width 0.5s ease; }
.spots-bar.low > i { background: var(--warn); }
.spots-bar.full > i { background: var(--danger); }
.spots-text { font-size: 0.8rem; color: var(--muted); margin-top: 0.35rem; font-weight: 500; }
.spots-text b { color: var(--ink); }
.card-actions { margin-top: 0.6rem; }
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--ink-2); }
.empty-state a { color: var(--accent-d); font-weight: 600; }
/* Impact band */
.impact-band { background: linear-gradient(150deg, var(--brand-d), var(--brand)); color: #fff; margin-top: 3.5rem; padding: 3.2rem 0; }
.impact-inner { display: grid; grid-template-columns: 1fr 1fr; gap: 2.4rem; align-items: center; }
.impact-text h2 { font-size: clamp(1.6rem, 3.2vw, 2.2rem); font-weight: 600; margin-bottom: 0.7rem; }
.impact-text p { color: rgba(255, 255, 255, 0.86); max-width: 42ch; margin: 0 0 1.4rem; }
.impact-cards { list-style: none; display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; margin: 0; padding: 0; }
.impact-cards li { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.16); border-radius: var(--r-md); padding: 1.1rem; }
.impact-cards strong { display: block; font-family: var(--serif); font-size: 1.9rem; font-weight: 600; }
.impact-cards span { font-size: 0.85rem; color: rgba(255, 255, 255, 0.82); }
/* Footer */
.site-footer { padding: 2.2rem 0; border-top: 1px solid var(--line); background: var(--surface); }
.footer-inner { text-align: center; font-size: 0.86rem; color: var(--ink-2); }
.footer-inner p { margin: 0.2rem 0; }
/* Drawer */
.drawer-scrim {
position: fixed;
inset: 0;
background: rgba(42, 39, 34, 0.5);
backdrop-filter: blur(2px);
z-index: 60;
opacity: 0;
transition: opacity 0.25s ease;
}
.drawer-scrim.show { opacity: 1; }
.drawer {
position: fixed;
top: 0;
right: 0;
height: 100%;
width: min(480px, 100%);
background: var(--bg);
z-index: 70;
box-shadow: var(--shadow-lg);
transform: translateX(100%);
transition: transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
overflow-y: auto;
}
.drawer.show { transform: translateX(0); }
.drawer-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 2;
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
cursor: pointer;
box-shadow: var(--shadow-sm);
}
.drawer-close:hover { background: #fff; color: var(--danger); }
.drawer-body { padding: 0 0 2rem; }
.drawer-photo { height: 170px; display: flex; align-items: flex-end; padding: 1.2rem 1.5rem; color: #fff; position: relative; }
.drawer-photo::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 30%, rgba(0, 0, 0, 0.4)); }
.drawer-photo .d-cause { position: relative; z-index: 1; }
.drawer-content { padding: 1.4rem 1.5rem 0; }
.drawer-content h2 { font-size: 1.55rem; font-weight: 600; margin-bottom: 0.2rem; }
.drawer-org { color: var(--muted); font-weight: 500; margin-bottom: 1rem; }
.d-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.7rem; margin-bottom: 1.1rem; }
.d-fact { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md); padding: 0.7rem 0.85rem; }
.d-fact .k { font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); font-weight: 700; }
.d-fact .v { font-size: 0.95rem; font-weight: 600; color: var(--ink); }
.d-desc { color: var(--ink-2); margin-bottom: 1.2rem; }
.d-list { margin: 0 0 1.2rem; padding-left: 1.1rem; color: var(--ink-2); }
.d-list li { margin-bottom: 0.3rem; }
/* Apply form */
.apply-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 1.3rem; box-shadow: var(--shadow-sm); }
.apply-card h3 { font-size: 1.2rem; font-weight: 600; margin-bottom: 0.2rem; }
.apply-card .form-sub { color: var(--muted); font-size: 0.88rem; margin-bottom: 1rem; }
.field { margin-bottom: 0.95rem; }
.field label { display: block; font-size: 0.86rem; font-weight: 600; margin-bottom: 0.35rem; }
.field .req { color: var(--accent-d); }
.field input[type="text"], .field input[type="email"], .field input[type="tel"], .field textarea, .field select {
width: 100%;
font: inherit;
padding: 0.65rem 0.8rem;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--bg);
color: var(--ink);
}
.field textarea { resize: vertical; min-height: 72px; }
.field input:focus, .field textarea:focus, .field select:focus { outline: 0; border-color: var(--brand); background: #fff; box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.14); }
.field.invalid input, .field.invalid textarea, .field.invalid .checks { border-color: var(--danger); }
.field-error { display: none; color: var(--danger); font-size: 0.8rem; margin-top: 0.3rem; font-weight: 500; }
.field.invalid .field-error { display: block; }
.checks { display: flex; flex-wrap: wrap; gap: 0.45rem; border: 1px solid transparent; border-radius: var(--r-sm); }
.check {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.84rem;
font-weight: 500;
padding: 0.4rem 0.75rem;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--bg);
cursor: pointer;
user-select: none;
}
.check input { position: absolute; opacity: 0; width: 0; height: 0; }
.check:hover { border-color: var(--brand); }
.check:has(input:checked) { background: var(--brand); border-color: var(--brand); color: #fff; }
.check:has(input:focus-visible) { outline: 3px solid var(--brand); outline-offset: 2px; }
.form-success { text-align: center; padding: 1.5rem 0.5rem; }
.form-success .tick { width: 56px; height: 56px; margin: 0 auto 0.8rem; border-radius: 50%; display: grid; place-items: center; background: rgba(47, 158, 111, 0.14); color: var(--ok); }
.form-success h3 { font-size: 1.35rem; margin-bottom: 0.4rem; }
.form-success p { color: var(--ink-2); }
/* Toasts */
.toast-stack { position: fixed; bottom: 1.2rem; left: 50%; transform: translateX(-50%); z-index: 90; display: flex; flex-direction: column; gap: 0.5rem; width: min(380px, calc(100% - 32px)); }
.toast {
background: var(--ink);
color: #fff;
padding: 0.8rem 1rem;
border-radius: var(--r-md);
box-shadow: var(--shadow-lg);
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.55rem;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.ok { background: var(--ok); }
.toast.warn { background: var(--warn); }
.toast .t-ico { flex: none; }
@media (max-width: 900px) {
.hero-inner { grid-template-columns: 1fr; gap: 1.8rem; }
.impact-inner { grid-template-columns: 1fr; gap: 1.6rem; }
}
@media (max-width: 520px) {
.wrap { width: calc(100% - 28px); }
.header-nav a:not(.btn) { display: none; }
.hero { padding: 2.4rem 0 1.8rem; }
.hero-stats { grid-template-columns: 1fr 1fr; padding: 1.2rem; }
.section-head { margin-top: 2rem; }
.search-box { min-width: 0; width: 100%; }
.grid { grid-template-columns: 1fr; }
.impact-cards { grid-template-columns: 1fr; }
.d-grid { grid-template-columns: 1fr; }
.drawer { width: 100%; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var CAUSE_META = {
hunger: { label: "Hunger", g: "linear-gradient(135deg,#e8743b,#cc5d28)" },
education: { label: "Education", g: "linear-gradient(135deg,#3d8bcf,#2a6aa3)" },
environment: { label: "Environment", g: "linear-gradient(135deg,#1f7a6d,#155e54)" },
housing: { label: "Housing", g: "linear-gradient(135deg,#9a6b3d,#7a522d)" },
health: { label: "Health", g: "linear-gradient(135deg,#b8506b,#8f3b53)" }
};
var COMMIT_LABEL = {
"one-time": "One-time",
"weekly": "Weekly",
"flexible": "Flexible hrs"
};
var OPPS = [
{ id: "o1", title: "Community Kitchen Cook", org: "Riverline Hope Kitchen", cause: "hunger",
commitment: "weekly", location: "onsite", area: "Riverside Hall", remote: false,
hours: "Sat mornings · 4 hrs", skills: ["Cooking", "Teamwork"], capacity: 12, filled: 9,
photoCap: "Volunteers plating 300 meals",
desc: "Help prepare and serve free hot meals to neighbors facing food insecurity. No professional experience needed — our chefs guide every shift.",
tasks: ["Chop, prep and cook from a set menu", "Plate and serve with our front-of-house team", "Wipe down and reset the kitchen for the next crew"] },
{ id: "o2", title: "After-School Reading Buddy", org: "Eastvale Learning Center", cause: "education",
commitment: "weekly", location: "onsite", area: "Eastvale Library", remote: false,
hours: "Tue & Thu · 3–5pm", skills: ["Tutoring", "Patience"], capacity: 20, filled: 14,
photoCap: "1:1 tutoring with grade-2 readers",
desc: "Sit one-on-one with a child and build their confidence through 30-minute reading sessions. Training and books provided.",
tasks: ["Read aloud and listen with young learners", "Log progress on a simple sheet", "Cheer on small wins"] },
{ id: "o3", title: "Riverbank Cleanup Crew", org: "Green Riverline Project", cause: "environment",
commitment: "one-time", location: "onsite", area: "North Bend Trail", remote: false,
hours: "Sun · 9am–1pm", skills: ["Outdoors", "Stamina"], capacity: 40, filled: 38,
photoCap: "Clearing 2 miles of shoreline",
desc: "Spend a morning outdoors pulling litter and invasive growth from the river’s edge. Gloves, bags and snacks are on us.",
tasks: ["Collect and sort litter into recycling streams", "Tag invasive plants for the restoration team", "Plant native grasses along bare banks"] },
{ id: "o4", title: "Move-In Day Helper", org: "Doorway Home Partners", cause: "housing",
commitment: "one-time", location: "onsite", area: "Maple District", remote: false,
hours: "Flexible · 5 hrs", skills: ["Heavy lifting", "Driving"], capacity: 8, filled: 8,
photoCap: "Welcoming a family into their first home",
desc: "Help a family settle into permanent housing — carry boxes, build basic furniture and set up a warm welcome.",
tasks: ["Load, transport and unload belongings", "Assemble beds and shelving", "Stock a welcome-home pantry box"] },
{ id: "o5", title: "Remote Grant Writer", org: "Riverline Hope HQ", cause: "education",
commitment: "flexible", location: "remote", area: "Remote", remote: true,
hours: "~5 hrs / week", skills: ["Writing", "Research"], capacity: 5, filled: 2,
photoCap: "Funding the next 1,000 meals",
desc: "Use your words from anywhere. Research funders and draft compelling grant applications that keep our programs running.",
tasks: ["Research aligned foundations and deadlines", "Draft and edit grant narratives", "Track submissions in our shared tracker"] },
{ id: "o6", title: "Wellness Check Caller", org: "Neighbors Care Line", cause: "health",
commitment: "weekly", location: "remote", area: "Remote", remote: true,
hours: "2 evenings / week", skills: ["Listening", "Empathy"], capacity: 16, filled: 7,
photoCap: "Friendly calls to isolated elders",
desc: "Phone older neighbors living alone for a friendly chat and a safety check. Training, scripts and a coordinator support you.",
tasks: ["Make scheduled wellness calls", "Note any needs for our care team", "Brighten someone’s day with conversation"] },
{ id: "o7", title: "Mobile Pantry Driver", org: "Riverline Hope Kitchen", cause: "hunger",
commitment: "flexible", location: "onsite", area: "9 neighborhoods", remote: false,
hours: "Choose your route", skills: ["Driving", "Logistics"], capacity: 10, filled: 6,
photoCap: "Groceries delivered door to door",
desc: "Deliver boxes of fresh groceries to homebound families on a route that fits your week. A valid license is required.",
tasks: ["Pick up packed boxes from the depot", "Follow a delivery route with our app", "Confirm deliveries with a friendly hello"] },
{ id: "o8", title: "Community Garden Mentor", org: "Green Riverline Project", cause: "environment",
commitment: "weekly", location: "onsite", area: "Southgate Lot", remote: false,
hours: "Wed mornings · 3 hrs", skills: ["Gardening", "Teaching"], capacity: 14, filled: 4,
photoCap: "Growing food and confidence together",
desc: "Share your green thumb with families learning to grow their own food in our shared plots. Beginners welcome to mentor beginners.",
tasks: ["Guide planting, watering and harvesting", "Maintain shared tools and compost", "Host short ‘grow your own’ chats"] },
{ id: "o9", title: "Event Photographer", org: "Riverline Hope HQ", cause: "education",
commitment: "one-time", location: "onsite", area: "Riverside Hall", remote: false,
hours: "Gala night · 4 hrs", skills: ["Photography", "Editing"], capacity: 4, filled: 1,
photoCap: "Telling our story in pictures",
desc: "Capture the warmth of our spring gala so we can share the impact with donors and the community.",
tasks: ["Shoot candid and posed moments", "Deliver edited highlights within a week", "Tag photos for our media library"] }
];
/* ---------- Helpers ---------- */
function $(sel, root) { return (root || document).querySelector(sel); }
function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
var toastStack = $("#toastStack");
function toast(msg, kind) {
var t = el("div", "toast" + (kind ? " " + kind : ""));
var ico = kind === "ok" ? "✓" : kind === "warn" ? "!" : "♡";
t.innerHTML = '<span class="t-ico" aria-hidden="true">' + ico + "</span><span>" + msg + "</span>";
toastStack.appendChild(t);
requestAnimationFrame(function () { t.classList.add("show"); });
setTimeout(function () {
t.classList.remove("show");
setTimeout(function () { t.remove(); }, 280);
}, 3200);
}
var ICON = {
pin: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>',
clock: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
repeat: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>'
};
/* ---------- Filter state ---------- */
var state = { cause: "all", commitment: "all", location: "all", q: "" };
var grid = $("#cardGrid");
var emptyState = $("#emptyState");
var resultCount = $("#resultCount");
var clearBtn = $("#clearFilters");
function matches(o) {
if (state.cause !== "all" && o.cause !== state.cause) return false;
if (state.commitment !== "all" && o.commitment !== state.commitment) return false;
if (state.location !== "all" && o.location !== state.location) return false;
if (state.q) {
var hay = (o.title + " " + o.org + " " + o.area + " " + o.skills.join(" ")).toLowerCase();
if (hay.indexOf(state.q) === -1) return false;
}
return true;
}
function spotsClass(o) {
var left = o.capacity - o.filled;
if (left <= 0) return "full";
if (left <= Math.max(2, Math.ceil(o.capacity * 0.2))) return "low";
return "";
}
function cardHTML(o) {
var meta = CAUSE_META[o.cause];
var left = o.capacity - o.filled;
var pct = Math.round((o.filled / o.capacity) * 100);
var sc = spotsClass(o);
var card = el("article", "card");
card.dataset.id = o.id;
var spotsText = left <= 0
? '<span style="color:var(--danger);font-weight:700">Fully staffed — join the waitlist</span>'
: "<b>" + left + " spot" + (left === 1 ? "" : "s") + " left</b> of " + o.capacity;
card.innerHTML =
'<div class="card-photo" style="background:' + meta.g + '">' +
'<span class="cause-tag">' + meta.label + "</span>" +
(o.remote ? '<span class="remote-tag">Remote</span>' : "") +
'<span class="photo-cap">' + o.photoCap + "</span>" +
"</div>" +
'<div class="card-body">' +
"<h3>" + o.title + "</h3>" +
'<p class="card-org">' + o.org + "</p>" +
'<div class="card-meta">' +
"<span>" + ICON.pin + o.area + "</span>" +
"<span>" + ICON.clock + o.hours + "</span>" +
"<span>" + ICON.repeat + COMMIT_LABEL[o.commitment] + "</span>" +
"</div>" +
'<div class="skills">' + o.skills.map(function (s) { return '<span class="skill-pill">' + s + "</span>"; }).join("") + "</div>" +
'<div class="spots">' +
'<div class="spots-bar ' + sc + '"><i style="width:' + pct + '%"></i></div>' +
'<p class="spots-text">' + spotsText + "</p>" +
"</div>" +
'<div class="card-actions">' +
'<button class="btn btn-brand btn-block" data-open="' + o.id + '">' +
(left <= 0 ? "View & join waitlist" : "View & apply") +
"</button>" +
"</div>" +
"</div>";
return card;
}
function render() {
var list = OPPS.filter(matches);
grid.innerHTML = "";
list.forEach(function (o) { grid.appendChild(cardHTML(o)); });
emptyState.hidden = list.length !== 0;
resultCount.textContent = list.length === 0
? "No roles found"
: "Showing " + list.length + " of " + OPPS.length + " open roles";
var active = state.cause !== "all" || state.commitment !== "all" || state.location !== "all" || state.q !== "";
clearBtn.hidden = !active;
}
/* ---------- Filter wiring ---------- */
document.querySelectorAll(".filter-group").forEach(function (group) {
var key = group.dataset.filter;
group.addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
group.querySelectorAll(".chip").forEach(function (c) { c.classList.remove("is-active"); });
chip.classList.add("is-active");
state[key] = chip.dataset.value;
render();
});
});
$("#searchInput").addEventListener("input", function (e) {
state.q = e.target.value.trim().toLowerCase();
render();
});
function resetFilters() {
state = { cause: "all", commitment: "all", location: "all", q: "" };
$("#searchInput").value = "";
document.querySelectorAll(".filter-group").forEach(function (group) {
group.querySelectorAll(".chip").forEach(function (c, i) { c.classList.toggle("is-active", i === 0); });
});
render();
}
clearBtn.addEventListener("click", resetFilters);
$("#emptyReset").addEventListener("click", function (e) { e.preventDefault(); resetFilters(); });
/* ---------- Drawer ---------- */
var scrim = $("#scrim");
var drawer = $("#drawer");
var drawerBody = $("#drawerBody");
var lastFocus = null;
function openDrawer(id) {
var o = OPPS.find(function (x) { return x.id === id; });
if (!o) return;
lastFocus = document.activeElement;
var meta = CAUSE_META[o.cause];
var left = o.capacity - o.filled;
var isFull = left <= 0;
drawerBody.innerHTML =
'<div class="drawer-photo" style="background:' + meta.g + '">' +
'<span class="cause-tag d-cause">' + meta.label + "</span>" +
"</div>" +
'<div class="drawer-content">' +
'<h2 id="drawerTitle">' + o.title + "</h2>" +
'<p class="drawer-org">' + o.org + "</p>" +
'<div class="d-grid">' +
fact("Where", o.remote ? "Remote" : o.area) +
fact("When", o.hours) +
fact("Commitment", COMMIT_LABEL[o.commitment]) +
fact("Openings", isFull ? "Waitlist only" : left + " of " + o.capacity) +
"</div>" +
'<p class="d-desc">' + o.desc + "</p>" +
"<h3 style=\"font-family:var(--serif);font-size:1.05rem;margin-bottom:.4rem\">What you'll do</h3>" +
'<ul class="d-list">' + o.tasks.map(function (t) { return "<li>" + t + "</li>"; }).join("") + "</ul>" +
'<div class="skills" style="margin-bottom:1.3rem">' + o.skills.map(function (s) { return '<span class="skill-pill">' + s + "</span>"; }).join("") + "</div>" +
applyFormHTML(o, isFull) +
"</div>";
wireForm(o, isFull);
scrim.hidden = false;
drawer.hidden = false;
requestAnimationFrame(function () {
scrim.classList.add("show");
drawer.classList.add("show");
});
document.body.style.overflow = "hidden";
setTimeout(function () { $("#drawerTitle").setAttribute("tabindex", "-1"); $("#drawerTitle").focus(); }, 60);
}
function fact(k, v) {
return '<div class="d-fact"><div class="k">' + k + '</div><div class="v">' + v + "</div></div>";
}
function applyFormHTML(o, isFull) {
return '<div class="apply-card">' +
"<h3>" + (isFull ? "Join the waitlist" : "Apply for this role") + "</h3>" +
'<p class="form-sub">' + (isFull
? "This role is full — we’ll reach out the moment a spot opens."
: "Tell us a little about you. We’ll confirm within 2 working days.") + "</p>" +
'<form id="applyForm" novalidate>' +
field("vName", "text", "Full name", true) +
field("vEmail", "email", "Email address", true) +
field("vPhone", "tel", "Phone (optional)", false) +
'<div class="field" id="f-avail">' +
'<label>Availability <span class="req">*</span></label>' +
'<div class="checks" role="group" aria-label="Availability">' +
availBox("Weekday mornings") + availBox("Weekday evenings") +
availBox("Weekends") + availBox("Flexible / on-call") +
"</div>" +
'<p class="field-error">Pick at least one time you can help.</p>' +
"</div>" +
'<div class="field">' +
'<label for="vWhy">Why this role? (optional)</label>' +
'<textarea id="vWhy" placeholder="A sentence or two about what draws you in…"></textarea>' +
"</div>" +
'<button type="submit" class="btn btn-accent btn-block">' +
(isFull ? "Add me to the waitlist" : "Submit application") +
"</button>" +
"</form>" +
"</div>";
}
function field(id, type, label, required) {
return '<div class="field" id="f-' + id + '">' +
'<label for="' + id + '">' + label + (required ? ' <span class="req">*</span>' : "") + "</label>" +
'<input id="' + id + '" type="' + type + '" />' +
'<p class="field-error">Please enter a valid ' + label.toLowerCase().replace(" (optional)", "") + ".</p>" +
"</div>";
}
function availBox(label) {
return '<label class="check"><input type="checkbox" value="' + label + '" />' + label + "</label>";
}
function wireForm(o, isFull) {
var form = $("#applyForm");
if (!form) return;
form.addEventListener("submit", function (e) {
e.preventDefault();
var ok = true;
var name = $("#vName");
var fName = $("#f-vName");
if (!name.value.trim()) { fName.classList.add("invalid"); ok = false; } else { fName.classList.remove("invalid"); }
var email = $("#vEmail");
var fEmail = $("#f-vEmail");
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) { fEmail.classList.add("invalid"); ok = false; } else { fEmail.classList.remove("invalid"); }
var avail = form.querySelectorAll('#f-avail input:checked');
var fAvail = $("#f-avail");
if (avail.length === 0) { fAvail.classList.add("invalid"); ok = false; } else { fAvail.classList.remove("invalid"); }
if (!ok) {
toast("Please fix the highlighted fields.", "warn");
var firstBad = form.querySelector(".invalid input");
if (firstBad) firstBad.focus();
return;
}
// success — update capacity if a real spot
if (!isFull) {
o.filled = Math.min(o.capacity, o.filled + 1);
render();
}
var first = name.value.trim().split(" ")[0];
$(".apply-card").innerHTML =
'<div class="form-success">' +
'<div class="tick"><svg viewBox="0 0 24 24" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg></div>' +
"<h3>Thank you, " + first + "!</h3>" +
"<p>" + (isFull
? "You’re on the waitlist for <strong>" + o.title + "</strong>. We’ll email you the moment a spot opens."
: "Your application for <strong>" + o.title + "</strong> is in. A coordinator will confirm within 2 working days.") + "</p>" +
"</div>";
toast(isFull ? "Added to the waitlist — thank you!" : "Application submitted — welcome aboard!", "ok");
});
// clear error on input
form.addEventListener("input", function (e) {
var f = e.target.closest(".field");
if (f) f.classList.remove("invalid");
});
}
function closeDrawer() {
scrim.classList.remove("show");
drawer.classList.remove("show");
document.body.style.overflow = "";
setTimeout(function () {
scrim.hidden = true;
drawer.hidden = true;
drawerBody.innerHTML = "";
if (lastFocus && lastFocus.focus) lastFocus.focus();
}, 320);
}
grid.addEventListener("click", function (e) {
var btn = e.target.closest("[data-open]");
if (btn) openDrawer(btn.dataset.open);
});
$("#drawerClose").addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !drawer.hidden) closeDrawer();
});
/* ---------- Animated counters ---------- */
function animateCount(node) {
var target = parseInt(node.dataset.count, 10);
var prefix = node.dataset.prefix || "";
var suffix = node.dataset.suffix || "";
var start = performance.now();
var dur = 1400;
function step(now) {
var p = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = Math.round(target * eased);
node.textContent = prefix + val.toLocaleString("en-US") + suffix;
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
var counters = document.querySelectorAll(".count");
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries, obs) {
entries.forEach(function (en) {
if (en.isIntersecting) { animateCount(en.target); obs.unobserve(en.target); }
});
}, { threshold: 0.4 });
counters.forEach(function (c) { io.observe(c); });
} else {
counters.forEach(animateCount);
}
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Riverline Hope — Volunteer With Us</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:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#opportunities">Skip to opportunities</a>
<header class="site-header">
<div class="wrap header-inner">
<a class="brand" href="#" aria-label="Riverline Hope home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-4.35-9.5-8.5C.8 9.6 2.2 6 5.5 6 7.6 6 9 7.4 12 10c3-2.6 4.4-4 6.5-4 3.3 0 4.7 3.6 3 6.5C19 16.65 12 21 12 21Z"/></svg>
</span>
<span class="brand-text">Riverline <em>Hope</em></span>
</a>
<nav class="header-nav" aria-label="Primary">
<a href="#opportunities">Opportunities</a>
<a href="#impact">Our impact</a>
<a href="#" class="btn btn-accent btn-sm">Donate</a>
</nav>
</div>
</header>
<section class="hero">
<div class="wrap hero-inner">
<div class="hero-copy">
<span class="eyebrow">Volunteer · Riverside, Eastvale</span>
<h1>Give a few hours. Change a whole neighborhood.</h1>
<p class="lede">Riverline Hope connects thousands of volunteers to hands-on work feeding families, tutoring kids, restoring rivers, and standing alongside neighbors in need. Find a role that fits your skills and schedule.</p>
<div class="hero-actions">
<a href="#opportunities" class="btn btn-brand">Browse opportunities</a>
<a href="#" class="btn btn-ghost">Talk to our team</a>
</div>
<ul class="trust-row" aria-label="Trust signals">
<li><span class="badge-dot" aria-hidden="true"></span> Registered charity #84-2210561</li>
<li><span class="badge-dot" aria-hidden="true"></span> Background-checked roles</li>
<li><span class="badge-dot" aria-hidden="true"></span> Volunteer hours certified</li>
</ul>
</div>
<aside class="hero-stats" aria-label="Volunteer impact this year">
<div class="stat">
<strong class="count" data-count="3120">0</strong>
<span>active volunteers</span>
</div>
<div class="stat">
<strong class="count" data-count="41800" data-suffix="">0</strong>
<span>hours given this year</span>
</div>
<div class="stat">
<strong class="count" data-count="186000" data-prefix="" data-suffix="+">0</strong>
<span>meals served</span>
</div>
<div class="stat stat-wide">
<strong class="count" data-count="62">0</strong>
<span>partner organizations across 9 neighborhoods</span>
</div>
</aside>
</div>
</section>
<main id="opportunities" class="wrap" tabindex="-1">
<div class="section-head">
<div>
<h2>Open volunteer opportunities</h2>
<p class="muted" id="resultCount">Loading roles…</p>
</div>
<div class="search-box">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="search" id="searchInput" placeholder="Search roles, skills…" aria-label="Search opportunities" />
</div>
</div>
<div class="filters" role="region" aria-label="Filter opportunities">
<div class="filter-group" data-filter="cause" role="group" aria-label="Cause">
<span class="filter-label">Cause</span>
<button class="chip is-active" data-value="all">All</button>
<button class="chip" data-value="hunger">Hunger</button>
<button class="chip" data-value="education">Education</button>
<button class="chip" data-value="environment">Environment</button>
<button class="chip" data-value="housing">Housing</button>
<button class="chip" data-value="health">Health</button>
</div>
<div class="filter-group" data-filter="commitment" role="group" aria-label="Time commitment">
<span class="filter-label">Time</span>
<button class="chip is-active" data-value="all">Any</button>
<button class="chip" data-value="one-time">One-time</button>
<button class="chip" data-value="weekly">Weekly</button>
<button class="chip" data-value="flexible">Flexible</button>
</div>
<div class="filter-group" data-filter="location" role="group" aria-label="Location">
<span class="filter-label">Where</span>
<button class="chip is-active" data-value="all">Anywhere</button>
<button class="chip" data-value="onsite">On-site</button>
<button class="chip" data-value="remote">Remote</button>
</div>
<button class="link-reset" id="clearFilters" type="button" hidden>Clear filters</button>
</div>
<div class="grid" id="cardGrid" aria-live="polite"></div>
<p class="empty-state" id="emptyState" hidden>
<strong>No roles match those filters.</strong><br />
Try widening your search — or <a href="#" id="emptyReset">reset all filters</a>.
</p>
</main>
<section id="impact" class="impact-band">
<div class="wrap impact-inner">
<div class="impact-text">
<span class="eyebrow light">Why it matters</span>
<h2>Last spring, volunteers like you made this real.</h2>
<p>Every role on this page traces back to a neighbor who needed a hand. Here is what the community built together — in just one season.</p>
<a href="#opportunities" class="btn btn-accent">Find your role</a>
</div>
<ul class="impact-cards">
<li><strong>9,400</strong><span>warm meals packed and delivered</span></li>
<li><strong>1,250</strong><span>hours of free tutoring for kids</span></li>
<li><strong>14 mi</strong><span>of riverbank cleared of litter</span></li>
<li><strong>320</strong><span>families helped to move into homes</span></li>
</ul>
</div>
</section>
<footer class="site-footer">
<div class="wrap footer-inner">
<p>© 2026 Riverline Hope Community Foundation · A fictional 501(c)(3) for demonstration.</p>
<p class="muted">Tax-deductible to the extent allowed by law · EIN 84-2210561</p>
</div>
</footer>
<!-- Opportunity detail + apply drawer -->
<div class="drawer-scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" hidden>
<button class="drawer-close" id="drawerClose" type="button" aria-label="Close panel">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button>
<div class="drawer-body" id="drawerBody"><!-- injected --></div>
</aside>
<div class="toast-stack" id="toastStack" aria-live="assertive" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Volunteer Signup
A get-involved page for the fictional Riverline Hope community foundation, built in the warm, human nonprofit palette — mission teal-green, a donate-orange accent and a humane Fraunces serif over Inter. The hero pairs a clear call to browse roles with transparency cues: animated impact counters (active volunteers, hours given, meals served) and trust badges for registered-charity status, background-checked roles and certified hours. Each open opportunity renders as a photo card showing its cause tag, hosting organization, location, schedule and required skills, plus a live capacity bar that turns amber when only a few spots remain and red when a role is fully staffed.
Chip filters narrow the list by cause, time commitment (one-time, weekly, flexible) and on-site versus remote, and a text search matches across titles, skills and neighborhoods in real time. A clear-filters control and a friendly empty state appear when no roles match. Clicking any card slides open a right-hand detail drawer with the full description, a what-you-will-do checklist and an apply form.
The apply form validates a name, a real email address and at least one availability window, surfacing inline errors and a warning toast on failure. A successful submission decrements the role’s remaining spots, swaps the form for a personalized thank-you, and confirms with a success toast — automatically offering a waitlist instead when the role is already full. The drawer closes on the scrim, the close button or the Escape key, returning focus to the card that opened it, and the whole layout collapses cleanly to a single column down to 360px.
Illustrative UI only — fictional organization, not a real charity or donation system.