Construction — Client Portal
A homeowner client portal dashboard for a remodel project showing live build progress, upcoming milestones, recent site photos, permits and documents, an invoices and payments ledger, and a threaded message panel to reach the project manager, all wired with vanilla JS.
MCP
Code
:root {
--hv: #f5c518;
--steel: #2b3440;
--steel-d: #1b222b;
--bg: #eceae6;
--ink: #1a1d22;
--muted: #666c75;
--orange: #e8642a;
--white: #ffffff;
--line: #d7d4ce;
--green: #2f8f54;
--radius: 10px;
--shadow: 0 2px 0 rgba(27, 34, 43, 0.08), 0 8px 24px rgba(27, 34, 43, 0.07);
--font: 'Inter', system-ui, -apple-system, sans-serif;
--display: 'Oswald', 'Inter', system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.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;
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--hv);
color: var(--steel-d);
padding: .6rem 1rem;
font-weight: 700;
z-index: 50;
}
.skip-link:focus { left: 8px; top: 8px; }
a { color: var(--orange); }
:focus-visible {
outline: 3px solid var(--hv);
outline-offset: 2px;
}
/* ---------- topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem 1.4rem;
background: var(--steel-d);
color: var(--white);
border-bottom: 4px solid var(--hv);
}
.topbar__brand { display: flex; align-items: center; gap: .7rem; }
.topbar__brand strong { display: block; font-family: var(--display); letter-spacing: .5px; font-size: 1.05rem; }
.topbar__sub { font-size: .72rem; text-transform: uppercase; letter-spacing: 2px; color: var(--hv); }
.logo {
display: grid; place-items: center;
width: 38px; height: 38px;
background: var(--hv);
color: var(--steel-d);
border-radius: 8px;
font-size: 1.3rem;
}
.topbar__client { display: flex; align-items: center; gap: .7rem; }
.topbar__greet { font-size: .9rem; color: #c8ccd2; }
.avatar {
display: grid; place-items: center;
width: 38px; height: 38px;
border-radius: 50%;
background: var(--orange);
color: var(--white);
font-weight: 700;
font-size: .85rem;
}
/* ---------- layout ---------- */
.wrap { max-width: 1080px; margin: 0 auto; padding: 1.6rem 1.4rem 4rem; }
.page-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(1.6rem, 4vw, 2.3rem);
text-transform: uppercase;
letter-spacing: .5px;
margin: 0 0 .25rem;
}
.page-meta { color: var(--muted); margin: 0 0 1.4rem; font-size: .92rem; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.2rem;
}
/* ---------- cards ---------- */
.card {
position: relative;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1.25rem 1.3rem;
box-shadow: var(--shadow);
overflow: hidden;
}
.card h2 {
font-family: var(--display);
text-transform: uppercase;
letter-spacing: .6px;
font-size: 1.1rem;
margin: 0 0 .9rem;
}
.card__sub { color: var(--muted); font-size: .85rem; margin: -.6rem 0 .9rem; }
.status { grid-column: 1 / -1; }
.invoices { grid-column: 1 / -1; }
.messages { grid-column: 1 / -1; }
/* hazard accent */
.hazard {
position: absolute;
top: 0; left: 0; right: 0;
height: 6px;
background: repeating-linear-gradient(
-45deg,
var(--hv) 0 14px,
var(--steel-d) 14px 28px
);
}
.status { padding-top: 1.5rem; }
.status__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.status__head h2 { margin: 0; }
.status__phase { margin: .3rem 0 0; color: var(--muted); font-size: .92rem; }
/* badges */
.badge {
display: inline-block;
padding: .25rem .6rem;
border-radius: 6px;
font-size: .72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .5px;
background: #e4e2dd;
color: var(--muted);
white-space: nowrap;
}
.badge--active { background: #fbeec2; color: #7a5b00; }
.badge--done { background: #d9efe0; color: var(--green); }
/* progress */
.progress {
height: 18px;
background: #e2dfd9;
border-radius: 9px;
overflow: hidden;
border: 1px solid var(--line);
}
.progress__bar {
height: 100%;
width: var(--pct, 0%);
background: linear-gradient(90deg, var(--orange), var(--hv));
transition: width 1.1s cubic-bezier(.2,.8,.2,1);
}
.progress__label { margin: .55rem 0 0; font-size: .9rem; color: var(--muted); }
.progress__label strong { color: var(--ink); font-size: 1.05rem; }
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
margin: 1.2rem 0 0;
background: var(--line);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.stats > div { background: #fafaf8; padding: .8rem .9rem; }
.stats dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin: 0; }
.stats dd { margin: .25rem 0 0; font-family: var(--display); font-size: 1.15rem; font-weight: 600; }
/* ---------- milestones ---------- */
.timeline { list-style: none; margin: 0; padding: 0; }
.tl {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: .8rem;
padding: .7rem 0;
border-bottom: 1px dashed var(--line);
position: relative;
}
.tl:last-child { border-bottom: 0; }
.tl strong { display: block; font-size: .95rem; }
.tl__date { font-size: .8rem; color: var(--muted); }
.tl__dot {
width: 14px; height: 14px;
border-radius: 50%;
background: var(--white);
border: 3px solid var(--line);
}
.tl--done .tl__dot { background: var(--green); border-color: var(--green); }
.tl--active .tl__dot { background: var(--hv); border-color: var(--orange); box-shadow: 0 0 0 4px rgba(245,197,24,.3); }
/* ---------- photos ---------- */
.gallery {
list-style: none; margin: 0; padding: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: .7rem;
}
.shot {
position: relative;
display: block;
width: 100%;
aspect-ratio: 4 / 3;
border: 0;
border-radius: 8px;
cursor: pointer;
color: var(--white);
font-family: var(--font);
font-weight: 600;
font-size: .82rem;
text-align: left;
padding: .7rem;
background: linear-gradient(140deg, var(--c1), var(--c2));
overflow: hidden;
transition: transform .18s ease, box-shadow .18s ease;
}
.shot span {
position: absolute;
left: .7rem; bottom: .6rem; right: .7rem;
text-shadow: 0 1px 4px rgba(0,0,0,.5);
}
.shot::before {
content: "📷";
position: absolute;
top: .5rem; right: .6rem;
opacity: .8;
}
.shot:hover, .shot:focus-visible { transform: translateY(-3px); box-shadow: var(--shadow); }
/* ---------- documents ---------- */
.doclist { list-style: none; margin: 0; padding: 0; }
.doclist li {
display: grid;
grid-template-columns: auto 1fr auto auto;
align-items: center;
gap: .8rem;
padding: .7rem 0;
border-bottom: 1px solid var(--line);
}
.doclist li:last-child { border-bottom: 0; }
.doc__icon {
display: grid; place-items: center;
width: 36px; height: 36px;
background: var(--steel);
color: var(--hv);
border-radius: 7px;
font-size: 1.1rem;
}
.doc__meta strong { display: block; font-size: .9rem; }
.doc__meta span { font-size: .78rem; color: var(--muted); }
.doc__link { font-size: .82rem; font-weight: 700; text-decoration: none; }
.doc__link:hover { text-decoration: underline; }
.tag {
font-size: .68rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .5px; padding: .2rem .5rem; border-radius: 5px;
background: #e4e2dd; color: var(--muted);
}
.tag--permit { background: #fbeec2; color: #7a5b00; }
.tag--contract { background: #dce4ef; color: #2f5288; }
.tag--plan { background: #e6dcef; color: #6b3f92; }
.tag--report { background: #d9efe0; color: var(--green); }
/* ---------- invoices ---------- */
.balance { font-size: .9rem; color: var(--muted); }
.balance strong { font-family: var(--display); font-size: 1.25rem; color: var(--orange); }
.inv-table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.inv-table th {
text-align: left;
font-size: .72rem;
text-transform: uppercase;
letter-spacing: .5px;
color: var(--muted);
padding: .55rem .6rem;
border-bottom: 2px solid var(--steel);
}
.inv-table td { padding: .7rem .6rem; border-bottom: 1px solid var(--line); }
.inv-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.inv-table tr.paid-anim td { background: #f4fbf6; }
.pay-btn {
border: 0;
background: var(--hv);
color: var(--steel-d);
font-family: var(--font);
font-weight: 700;
font-size: .8rem;
padding: .4rem .8rem;
border-radius: 6px;
cursor: pointer;
transition: filter .15s ease, transform .1s ease;
}
.pay-btn:hover { filter: brightness(1.06); }
.pay-btn:active { transform: translateY(1px); }
/* ---------- messages ---------- */
.thread { list-style: none; margin: 0 0 1rem; padding: 0; display: flex; flex-direction: column; gap: .7rem; max-height: 280px; overflow-y: auto; }
.msg {
max-width: 80%;
padding: .65rem .85rem;
border-radius: 12px;
font-size: .9rem;
}
.msg p { margin: .2rem 0 .3rem; }
.msg__who { font-size: .72rem; font-weight: 700; color: var(--muted); }
.msg time { font-size: .7rem; color: var(--muted); }
.msg--pm { align-self: flex-start; background: #f1efeb; border: 1px solid var(--line); border-bottom-left-radius: 3px; }
.msg--client { align-self: flex-end; background: var(--steel); color: var(--white); border-bottom-right-radius: 3px; }
.msg--client .msg__who, .msg--client time { color: #b7c0cb; }
.composer { display: flex; gap: .6rem; }
.composer input {
flex: 1;
font-family: var(--font);
font-size: .92rem;
padding: .7rem .85rem;
border: 2px solid var(--line);
border-radius: 8px;
background: #fafaf8;
}
.composer input:focus { border-color: var(--orange); outline: none; }
.composer button {
border: 0;
background: var(--orange);
color: var(--white);
font-family: var(--font);
font-weight: 700;
padding: 0 1.3rem;
border-radius: 8px;
cursor: pointer;
transition: filter .15s ease, transform .1s ease;
}
.composer button:hover { filter: brightness(1.06); }
.composer button:active { transform: translateY(1px); }
/* ---------- lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
background: rgba(15, 19, 24, .82);
display: grid;
place-items: center;
z-index: 100;
padding: 1.5rem;
}
.lightbox[hidden] { display: none; }
.lightbox__inner {
position: relative;
max-width: 560px;
width: 100%;
background: var(--steel-d);
border-radius: 12px;
padding: .6rem;
border: 3px solid var(--hv);
}
.lightbox__img { aspect-ratio: 4 / 3; border-radius: 8px; background: linear-gradient(140deg, #3d4b5a, #243038); }
.lightbox__cap { color: var(--white); font-size: .9rem; padding: .7rem .5rem .4rem; margin: 0; }
.lightbox__close {
position: absolute;
top: -14px; right: -14px;
width: 36px; height: 36px;
border-radius: 50%;
border: 3px solid var(--steel-d);
background: var(--hv);
color: var(--steel-d);
font-weight: 700;
cursor: pointer;
}
/* ---------- responsive ---------- */
@media (max-width: 760px) {
.grid { grid-template-columns: 1fr; }
.stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.topbar { padding: .7rem 1rem; }
.topbar__greet { display: none; }
.wrap { padding: 1.1rem 1rem 3rem; }
.stats { grid-template-columns: 1fr 1fr; }
.gallery { grid-template-columns: 1fr; }
.status__head { flex-direction: column; gap: .5rem; }
.doclist li { grid-template-columns: auto 1fr; row-gap: .3rem; }
.doclist .tag { grid-column: 2; justify-self: start; }
.doclist .doc__link { grid-column: 2; justify-self: start; }
.inv-table { font-size: .82rem; }
.inv-table th:nth-child(2), .inv-table td:nth-child(2) { display: none; }
.msg { max-width: 92%; }
}(function () {
"use strict";
/* ---- animate progress bar on load ---- */
var bar = document.querySelector(".progress__bar");
if (bar) {
var target = bar.style.getPropertyValue("--pct") || "62%";
bar.style.setProperty("--pct", "0%");
requestAnimationFrame(function () {
requestAnimationFrame(function () {
bar.style.setProperty("--pct", target);
});
});
}
/* ---- lightbox for site photos ---- */
var lightbox = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbCap = document.getElementById("lbCap");
var lbClose = document.getElementById("lbClose");
var lastFocus = null;
function openLightbox(btn) {
lastFocus = btn;
var bg = btn.style.background || getComputedStyle(btn).background;
lbImg.style.background = bg;
lbCap.textContent = btn.getAttribute("data-caption") || "";
lightbox.hidden = false;
lbClose.focus();
}
function closeLightbox() {
lightbox.hidden = true;
if (lastFocus) lastFocus.focus();
}
document.querySelectorAll(".shot").forEach(function (btn) {
btn.addEventListener("click", function () { openLightbox(btn); });
});
if (lbClose) lbClose.addEventListener("click", closeLightbox);
if (lightbox) {
lightbox.addEventListener("click", function (e) {
if (e.target === lightbox) closeLightbox();
});
}
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && lightbox && !lightbox.hidden) closeLightbox();
});
/* ---- pay invoices: settle balance + update running total ---- */
var balanceEl = document.getElementById("balance");
function parseMoney(str) {
return Number(String(str).replace(/[^0-9.]/g, "")) || 0;
}
function fmtMoney(n) {
return "$" + n.toLocaleString("en-US");
}
document.querySelectorAll(".pay-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var amount = Number(btn.getAttribute("data-amount")) || 0;
var row = btn.closest("tr");
// mark paid
var badge = document.createElement("span");
badge.className = "badge badge--done";
badge.textContent = "Paid";
btn.replaceWith(badge);
if (row) {
row.classList.add("paid-anim");
setTimeout(function () { row.classList.remove("paid-anim"); }, 1200);
}
// update outstanding balance
if (balanceEl) {
var current = parseMoney(balanceEl.textContent);
balanceEl.textContent = fmtMoney(Math.max(0, current - amount));
}
});
});
/* ---- message composer ---- */
var composer = document.getElementById("composer");
var input = document.getElementById("msgInput");
var thread = document.getElementById("thread");
function timeStamp() {
var d = new Date();
var h = d.getHours();
var m = String(d.getMinutes()).padStart(2, "0");
var ampm = h >= 12 ? "PM" : "AM";
h = h % 12 || 12;
var month = d.toLocaleString("en-US", { month: "short" });
return month + " " + d.getDate() + ", " + h + ":" + m + " " + ampm;
}
if (composer && input && thread) {
composer.addEventListener("submit", function (e) {
e.preventDefault();
var text = input.value.trim();
if (!text) return;
var li = document.createElement("li");
li.className = "msg msg--client";
li.innerHTML =
'<span class="msg__who">You</span><p></p><time></time>';
li.querySelector("p").textContent = text;
li.querySelector("time").textContent = timeStamp();
thread.appendChild(li);
input.value = "";
input.focus();
thread.scrollTop = thread.scrollHeight;
});
}
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Client Portal — Maple Street Build</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&family=Oswald:wght@500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar">
<div class="topbar__brand">
<span class="logo" aria-hidden="true">▰</span>
<div>
<strong>Ironvale Builders</strong>
<span class="topbar__sub">Client Portal</span>
</div>
</div>
<div class="topbar__client">
<span class="topbar__greet">Hi, Dana & Marcus Whitfield</span>
<span class="avatar" aria-hidden="true">DW</span>
</div>
</header>
<main id="main" class="wrap">
<h1 class="page-title">Maple Street Kitchen & Addition</h1>
<p class="page-meta">Project #IV-2274 · 418 Maple Street, Bristol, RI · PM: Tony Marchetti</p>
<div class="grid">
<!-- STATUS HERO -->
<section class="card status" aria-labelledby="status-h">
<div class="hazard" aria-hidden="true"></div>
<div class="status__head">
<div>
<h2 id="status-h">Build Progress</h2>
<p class="status__phase">Current phase: <strong>Framing & Rough-ins</strong></p>
</div>
<span class="badge badge--active">On schedule</span>
</div>
<div class="progress" role="img" aria-label="62 percent complete">
<div class="progress__bar" style="--pct:62%"></div>
</div>
<p class="progress__label"><strong id="pctLabel">62%</strong> complete · 23 of 37 work items closed</p>
<dl class="stats">
<div><dt>Contract value</dt><dd>$184,500</dd></div>
<div><dt>Started</dt><dd>Mar 4, 2026</dd></div>
<div><dt>Projected handover</dt><dd>Aug 28, 2026</dd></div>
<div><dt>Days on site</dt><dd>78</dd></div>
</dl>
</section>
<!-- MILESTONES -->
<section class="card milestones" aria-labelledby="ms-h">
<h2 id="ms-h">Milestones</h2>
<ol class="timeline">
<li class="tl tl--done">
<span class="tl__dot" aria-hidden="true"></span>
<div><strong>Demolition & site prep</strong><span class="tl__date">Mar 4 – Mar 18</span></div>
<span class="badge badge--done">Done</span>
</li>
<li class="tl tl--done">
<span class="tl__dot" aria-hidden="true"></span>
<div><strong>Foundation & slab pour</strong><span class="tl__date">Mar 20 – Apr 11</span></div>
<span class="badge badge--done">Done</span>
</li>
<li class="tl tl--active">
<span class="tl__dot" aria-hidden="true"></span>
<div><strong>Framing & rough-ins</strong><span class="tl__date">Apr 14 – Jun 27</span></div>
<span class="badge badge--active">In progress</span>
</li>
<li class="tl">
<span class="tl__dot" aria-hidden="true"></span>
<div><strong>Insulation & drywall</strong><span class="tl__date">Jul 1 – Jul 22</span></div>
<span class="badge">Upcoming</span>
</li>
<li class="tl">
<span class="tl__dot" aria-hidden="true"></span>
<div><strong>Cabinets, finishes & punch list</strong><span class="tl__date">Jul 25 – Aug 28</span></div>
<span class="badge">Upcoming</span>
</li>
</ol>
</section>
<!-- SITE PHOTOS -->
<section class="card photos" aria-labelledby="ph-h">
<h2 id="ph-h">Recent site photos</h2>
<p class="card__sub">Uploaded by the crew · tap to enlarge</p>
<ul class="gallery" id="gallery">
<li><button class="shot" data-caption="Rear addition framing topped out — Jun 18" style="--c1:#3d4b5a;--c2:#243038"><span>Framing topped out</span></button></li>
<li><button class="shot" data-caption="Kitchen rough plumbing inspected & passed — Jun 14" style="--c1:#4a5560;--c2:#2b3440"><span>Rough plumbing</span></button></li>
<li><button class="shot" data-caption="New header beam set over the great-room opening — Jun 10" style="--c1:#5a4636;--c2:#332a20"><span>Header beam set</span></button></li>
<li><button class="shot" data-caption="Electrical panel upgraded to 200A — Jun 6" style="--c1:#37474a;--c2:#1f2b2c"><span>Panel upgrade</span></button></li>
</ul>
</section>
<!-- DOCUMENTS -->
<section class="card docs" aria-labelledby="doc-h">
<h2 id="doc-h">Documents & permits</h2>
<ul class="doclist">
<li>
<span class="doc__icon" aria-hidden="true">▤</span>
<div class="doc__meta"><strong>Building Permit — BP-2026-0418</strong><span>PDF · 240 KB · issued Feb 27, 2026</span></div>
<span class="tag tag--permit">Permit</span>
<a class="doc__link" href="#" aria-label="Download Building Permit">Download</a>
</li>
<li>
<span class="doc__icon" aria-hidden="true">▤</span>
<div class="doc__meta"><strong>Signed Contract & Scope</strong><span>PDF · 1.1 MB · signed Feb 24, 2026</span></div>
<span class="tag tag--contract">Contract</span>
<a class="doc__link" href="#" aria-label="Download Signed Contract">Download</a>
</li>
<li>
<span class="doc__icon" aria-hidden="true">▤</span>
<div class="doc__meta"><strong>Architectural Plans — Rev C</strong><span>PDF · 6.4 MB · updated Apr 2, 2026</span></div>
<span class="tag tag--plan">Plans</span>
<a class="doc__link" href="#" aria-label="Download Architectural Plans">Download</a>
</li>
<li>
<span class="doc__icon" aria-hidden="true">▤</span>
<div class="doc__meta"><strong>Framing Inspection Report</strong><span>PDF · 180 KB · passed Jun 19, 2026</span></div>
<span class="tag tag--report">Report</span>
<a class="doc__link" href="#" aria-label="Download Framing Inspection Report">Download</a>
</li>
</ul>
</section>
<!-- INVOICES -->
<section class="card invoices" aria-labelledby="inv-h">
<div class="status__head">
<h2 id="inv-h">Invoices & payments</h2>
<span class="balance">Outstanding: <strong id="balance">$36,000</strong></span>
</div>
<table class="inv-table">
<caption class="sr-only">Project invoices and their payment status</caption>
<thead>
<tr><th scope="col">Invoice</th><th scope="col">Description</th><th scope="col">Due</th><th scope="col" class="num">Amount</th><th scope="col">Status</th></tr>
</thead>
<tbody id="invBody">
<tr><td>INV-001</td><td>Deposit & mobilization</td><td>Feb 28</td><td class="num">$36,900</td><td><span class="badge badge--done">Paid</span></td></tr>
<tr><td>INV-002</td><td>Foundation draw</td><td>Apr 12</td><td class="num">$46,125</td><td><span class="badge badge--done">Paid</span></td></tr>
<tr><td>INV-003</td><td>Framing draw</td><td>Jun 30</td><td class="num">$36,000</td><td><button class="pay-btn" data-amount="36000" data-row>Pay now</button></td></tr>
<tr><td>INV-004</td><td>Finishes draw</td><td>Aug 1</td><td class="num">$65,475</td><td><span class="badge">Scheduled</span></td></tr>
</tbody>
</table>
</section>
<!-- MESSAGES -->
<section class="card messages" aria-labelledby="msg-h">
<h2 id="msg-h">Messages with Tony</h2>
<ul class="thread" id="thread">
<li class="msg msg--pm">
<span class="msg__who">Tony Marchetti · PM</span>
<p>Framing inspection passed this morning — clean report, no corrections. Drywall is on track to start the week of July 1.</p>
<time>Jun 19, 8:42 AM</time>
</li>
<li class="msg msg--client">
<span class="msg__who">You</span>
<p>That's great news! Are we still good on the island countertop selection deadline?</p>
<time>Jun 19, 9:15 AM</time>
</li>
<li class="msg msg--pm">
<span class="msg__who">Tony Marchetti · PM</span>
<p>Yes — please confirm the quartz slab by July 5 so we can lock fabrication. I'll drop samples by Friday.</p>
<time>Jun 19, 9:31 AM</time>
</li>
</ul>
<form class="composer" id="composer">
<label class="sr-only" for="msgInput">Write a message to your project manager</label>
<input id="msgInput" type="text" placeholder="Message Tony…" autocomplete="off" required />
<button type="submit">Send</button>
</form>
</section>
</div>
</main>
<!-- LIGHTBOX -->
<div class="lightbox" id="lightbox" hidden>
<div class="lightbox__inner" role="dialog" aria-modal="true" aria-label="Site photo">
<div class="lightbox__img" id="lbImg"></div>
<p class="lightbox__cap" id="lbCap"></p>
<button class="lightbox__close" id="lbClose" aria-label="Close photo">✕</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Client Portal
A rugged, hi-vis dashboard a general contractor hands to a homeowner so they can follow a remodel without phone tag. The hero status card tracks the Maple Street Kitchen & Addition job with a percent-complete bar, current phase, contracted value and projected handover, while a sidebar of milestones shows what is done, in progress and still ahead.
Below the fold, a strip of recent site photos opens a lightbox, a documents list links permits and plans with type badges, and an invoices table totals what is paid versus outstanding with a live Pay now action that settles a balance and updates the running total. A message panel lets the client send a note to the project manager and see it appended to the thread instantly.
Everything is plain HTML, CSS custom properties and vanilla JavaScript — no frameworks, no build step. Realistic names, dates and dollar figures stand in for live data so you can wire it to a real backend later.
Illustrative UI only — not for real construction project management.