Law Firm — Client Portal
A logged-in client portal dashboard for a law firm showing a greeting, active matter summary cards, upcoming appointments, recent documents, outstanding invoices and firm messages, with tabbed sections, quick actions and live status badges built in vanilla JS.
MCP
Code
:root {
--navy: #1b2a4a;
--navy-d: #101b33;
--gold: #b08d57;
--gold-soft: #efe3cf;
--ink: #1a1f2b;
--muted: #5b6577;
--bg: #f5f3ee;
--white: #ffffff;
--line: #e3ddd0;
--line-soft: #ece7db;
--ok: #2f7d57;
--ok-bg: #e2efe7;
--alert: #b23b3b;
--alert-bg: #f6e3e1;
--warn: #8a6a2f;
--warn-bg: #f3e8cf;
--serif: "Georgia", "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--radius: 12px;
--shadow: 0 1px 2px rgba(16, 27, 51, 0.06), 0 8px 24px rgba(16, 27, 51, 0.06);
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
font-family: var(--sans);
color: var(--ink);
background: var(--bg);
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;
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- shell ---------- */
.portal {
min-height: 100vh;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 24px;
background: var(--navy);
color: var(--white);
border-bottom: 2px solid var(--gold);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border: 1px solid var(--gold);
color: var(--gold);
font-family: var(--serif);
font-weight: 700;
letter-spacing: 0.5px;
border-radius: 8px;
}
.brand__name {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand__name strong {
font-family: var(--serif);
font-size: 1.05rem;
}
.brand__name small {
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold-soft);
}
.topbar__right {
display: flex;
align-items: center;
gap: 16px;
}
.iconbtn {
position: relative;
display: grid;
place-items: center;
width: 38px;
height: 38px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: transparent;
color: var(--white);
border-radius: 9px;
transition: background 0.15s ease;
}
.iconbtn:hover {
background: rgba(255, 255, 255, 0.1);
}
.iconbtn__dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: var(--gold);
border-radius: 50%;
}
.iconbtn__dot.is-hidden {
display: none;
}
.who {
display: flex;
align-items: center;
gap: 10px;
}
.who__avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--gold-soft);
color: var(--navy-d);
font-weight: 600;
font-size: 0.85rem;
}
.who__meta {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.who__meta strong {
font-size: 0.9rem;
}
.who__meta small {
font-size: 0.74rem;
color: var(--gold-soft);
}
/* ---------- main ---------- */
.main {
max-width: 980px;
margin: 0 auto;
padding: 32px 24px 56px;
}
.greeting {
margin-bottom: 28px;
}
.greeting__eyebrow {
margin: 0 0 6px;
font-size: 0.72rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--gold);
font-weight: 600;
}
.greeting__title {
margin: 0 0 8px;
font-family: var(--serif);
font-size: clamp(1.6rem, 4vw, 2.1rem);
font-weight: 700;
color: var(--navy-d);
letter-spacing: -0.01em;
}
.greeting__sub {
margin: 0;
max-width: 60ch;
color: var(--muted);
}
.greeting__sub strong {
color: var(--ink);
}
/* ---------- summary cards ---------- */
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat {
background: var(--white);
border: 1px solid var(--line);
border-top: 3px solid var(--gold);
border-radius: var(--radius);
padding: 16px 16px 14px;
box-shadow: var(--shadow);
}
.stat--alert {
border-top-color: var(--alert);
}
.stat__label {
margin: 0 0 6px;
font-size: 0.74rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.stat__value {
margin: 0 0 4px;
font-family: var(--serif);
font-size: 1.7rem;
font-weight: 700;
color: var(--navy-d);
line-height: 1.1;
}
.stat__value--sm {
font-size: 1.05rem;
}
.stat__hint {
margin: 0;
font-size: 0.8rem;
color: var(--muted);
}
/* ---------- quick actions ---------- */
.quick {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 28px;
}
.quick__btn {
padding: 9px 16px;
border: 1px solid var(--navy);
background: var(--navy);
color: var(--white);
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
transition: background 0.15s ease, transform 0.1s ease;
}
.quick__btn:hover {
background: var(--navy-d);
}
.quick__btn:active {
transform: translateY(1px);
}
.quick__btn--ghost {
background: transparent;
color: var(--navy);
border-color: var(--line);
}
.quick__btn--ghost:hover {
background: var(--gold-soft);
border-color: var(--gold);
color: var(--navy-d);
}
/* ---------- panel + tabs ---------- */
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.tabs {
display: flex;
gap: 4px;
padding: 6px;
background: var(--gold-soft);
border-bottom: 1px solid var(--line);
overflow-x: auto;
}
.tab {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 16px;
border: 1px solid transparent;
background: transparent;
color: var(--navy-d);
border-radius: 8px;
font-size: 0.88rem;
font-weight: 500;
white-space: nowrap;
transition: background 0.15s ease;
}
.tab:hover {
background: rgba(255, 255, 255, 0.55);
}
.tab.is-active {
background: var(--white);
border-color: var(--line);
color: var(--navy-d);
font-weight: 600;
box-shadow: 0 1px 2px rgba(16, 27, 51, 0.08);
}
.tab__count {
display: inline-grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--navy);
color: var(--white);
font-size: 0.72rem;
font-weight: 600;
border-radius: 999px;
}
.tab.is-active .tab__count {
background: var(--gold);
color: var(--navy-d);
}
.pane {
padding: 20px 22px 24px;
}
.pane[hidden] {
display: none;
}
/* ---------- lists ---------- */
.list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.subhead {
margin: 26px 0 12px;
font-family: var(--serif);
font-size: 1.05rem;
color: var(--navy-d);
padding-bottom: 8px;
border-bottom: 1px solid var(--gold);
}
/* ---------- matter ---------- */
.matter {
border: 1px solid var(--line);
border-radius: 10px;
padding: 14px 16px;
background: var(--bg);
}
.matter__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.matter__title {
margin: 0;
font-family: var(--serif);
font-size: 1.02rem;
color: var(--navy-d);
}
.matter__ref {
margin: 4px 0 12px;
font-size: 0.8rem;
color: var(--muted);
letter-spacing: 0.02em;
}
.matter__meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 0;
}
.matter__meta div {
display: flex;
flex-direction: column;
}
.matter__meta dt {
font-size: 0.7rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.matter__meta dd {
margin: 2px 0 0;
font-size: 0.88rem;
color: var(--ink);
font-weight: 500;
}
/* ---------- appointment ---------- */
.appt {
display: flex;
gap: 14px;
align-items: flex-start;
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px 14px;
background: var(--white);
}
.appt__date {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 54px;
padding: 8px 0;
background: var(--navy);
color: var(--white);
border-radius: 8px;
line-height: 1;
}
.appt__date span {
font-size: 0.66rem;
letter-spacing: 0.1em;
color: var(--gold-soft);
}
.appt__date strong {
font-family: var(--serif);
font-size: 1.4rem;
margin-top: 2px;
}
.appt__title {
margin: 0 0 3px;
font-size: 0.96rem;
color: var(--navy-d);
}
.appt__meta {
margin: 0 0 8px;
font-size: 0.82rem;
color: var(--muted);
}
/* ---------- document ---------- */
.doc,
.inv,
.msg {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px 14px;
background: var(--white);
}
.doc__icon {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 42px;
height: 42px;
background: var(--gold-soft);
color: var(--warn);
border-radius: 8px;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.05em;
}
.doc__body,
.inv__body {
flex: 1 1 240px;
min-width: 0;
}
.doc__title,
.inv__title {
margin: 0 0 2px;
font-size: 0.94rem;
color: var(--navy-d);
}
.doc__meta,
.inv__meta {
margin: 0;
font-size: 0.8rem;
color: var(--muted);
}
/* ---------- invoice ---------- */
.inv__amount {
font-family: var(--serif);
font-size: 1.05rem;
font-weight: 700;
color: var(--navy-d);
}
.inv--paid .inv__amount {
color: var(--muted);
}
/* ---------- message ---------- */
.msg__avatar {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--navy);
color: var(--white);
font-size: 0.82rem;
font-weight: 600;
align-self: flex-start;
}
.msg__body {
flex: 1 1 240px;
min-width: 0;
}
.msg__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.msg__from {
margin: 0;
font-size: 0.92rem;
color: var(--navy-d);
}
.msg__time {
font-size: 0.76rem;
color: var(--muted);
}
.msg__text {
margin: 4px 0 8px;
font-size: 0.88rem;
color: var(--ink);
}
.msg--unread {
border-left: 3px solid var(--gold);
background: #fffdf8;
}
.msg.is-read {
opacity: 0.78;
}
.msg.is-read .msg__flag {
display: none;
}
/* ---------- badges ---------- */
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
border-radius: 999px;
white-space: nowrap;
}
.badge--active {
background: var(--ok-bg);
color: var(--ok);
}
.badge--action {
background: var(--warn-bg);
color: var(--warn);
}
.badge--hold {
background: #eceef2;
color: var(--muted);
}
.badge--neutral {
background: #eceef2;
color: var(--navy-d);
}
.badge--ok {
background: var(--ok-bg);
color: var(--ok);
}
.badge--overdue {
background: var(--alert-bg);
color: var(--alert);
}
.badge--video,
.badge--inperson {
background: var(--navy);
color: var(--gold-soft);
}
/* ---------- inline buttons ---------- */
.linkbtn {
flex: 0 0 auto;
border: 1px solid var(--line);
background: var(--white);
color: var(--navy);
font-size: 0.8rem;
font-weight: 500;
padding: 6px 12px;
border-radius: 8px;
transition: background 0.15s ease;
}
.linkbtn:hover:not(:disabled) {
background: var(--gold-soft);
border-color: var(--gold);
}
.linkbtn:disabled {
color: var(--muted);
cursor: default;
opacity: 0.7;
}
.paybtn {
flex: 0 0 auto;
border: 1px solid var(--gold);
background: var(--gold);
color: var(--navy-d);
font-size: 0.82rem;
font-weight: 600;
padding: 7px 16px;
border-radius: 8px;
transition: background 0.15s ease, transform 0.1s ease;
}
.paybtn:hover:not(:disabled) {
background: #9c7b48;
}
.paybtn:active:not(:disabled) {
transform: translateY(1px);
}
.paybtn:disabled {
background: var(--ok-bg);
border-color: var(--ok-bg);
color: var(--ok);
cursor: default;
}
.paybtn--confirm {
background: var(--alert);
border-color: var(--alert);
color: var(--white);
}
.paybtn--confirm:hover:not(:disabled) {
background: #963131;
}
/* ---------- disclaimer ---------- */
.disclaimer {
margin: 28px 0 0;
text-align: center;
font-size: 0.78rem;
font-style: italic;
color: var(--muted);
}
/* ---------- responsive ---------- */
@media (max-width: 880px) {
.summary {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.topbar {
padding: 12px 16px;
}
.who__meta {
display: none;
}
.main {
padding: 22px 16px 44px;
}
.summary {
grid-template-columns: 1fr;
}
.matter__meta {
grid-template-columns: 1fr;
}
.matter__head {
align-items: flex-start;
}
.doc__status,
.inv__status {
order: 3;
}
.linkbtn,
.paybtn {
width: 100%;
text-align: center;
}
.inv__amount {
margin-left: auto;
}
}(function () {
"use strict";
var live = document.getElementById("live");
function announce(msg) {
if (live) live.textContent = msg;
}
function money(n) {
return "$" + n.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
/* ---------- Tabs ---------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
var panes = {
matters: document.getElementById("pane-matters"),
documents: document.getElementById("pane-documents"),
invoices: document.getElementById("pane-invoices"),
messages: document.getElementById("pane-messages"),
};
function activate(name) {
tabs.forEach(function (tab) {
var on = tab.dataset.tab === name;
tab.classList.toggle("is-active", on);
tab.setAttribute("aria-selected", on ? "true" : "false");
tab.tabIndex = on ? 0 : -1;
});
Object.keys(panes).forEach(function (key) {
var pane = panes[key];
if (!pane) return;
var on = key === name;
pane.classList.toggle("is-active", on);
pane.hidden = !on;
});
}
tabs.forEach(function (tab, i) {
tab.addEventListener("click", function () {
activate(tab.dataset.tab);
});
tab.addEventListener("keydown", function (e) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
var dir = e.key === "ArrowRight" ? 1 : -1;
var next = (i + dir + tabs.length) % tabs.length;
tabs[next].focus();
activate(tabs[next].dataset.tab);
});
});
/* ---------- Quick actions jump to a tab ---------- */
document.querySelectorAll("[data-go]").forEach(function (btn) {
btn.addEventListener("click", function () {
var target = btn.dataset.go;
activate(target);
var pane = panes[target];
if (pane) pane.scrollIntoView({ behavior: "smooth", block: "start" });
});
});
/* ---------- Summary refs ---------- */
var statBalance = document.getElementById("statBalance");
var statBalanceHint = document.getElementById("statBalanceHint");
var statMessages = document.getElementById("statMessages");
var invCount = document.getElementById("invCount");
var msgCount = document.getElementById("msgCount");
var notifDot = document.getElementById("notifDot");
function recalcBalance() {
var due = document.querySelectorAll('.inv:not(.inv--paid)');
var total = 0;
due.forEach(function (el) {
total += Number(el.dataset.amount || 0);
});
if (statBalance) statBalance.textContent = money(total);
if (statBalanceHint) {
statBalanceHint.textContent =
due.length === 0
? "All invoices paid"
: due.length + " invoice" + (due.length === 1 ? "" : "s") + " due";
}
if (invCount) invCount.textContent = String(due.length);
}
function recalcMessages() {
var unread = document.querySelectorAll(".msg[data-unread]").length;
if (statMessages) statMessages.textContent = String(unread);
if (msgCount) msgCount.textContent = String(unread);
if (notifDot) notifDot.classList.toggle("is-hidden", unread === 0);
}
/* ---------- Pay invoice (inline confirm) ---------- */
document.querySelectorAll("[data-pay]").forEach(function (btn) {
btn.addEventListener("click", function () {
var row = btn.closest(".inv");
if (!row) return;
if (btn.dataset.confirming !== "true") {
btn.dataset.confirming = "true";
btn.dataset.label = btn.textContent;
btn.textContent = "Confirm payment";
btn.classList.add("paybtn--confirm");
announce("Press confirm to pay invoice " + (row.querySelector(".inv__title") ? row.querySelector(".inv__title").textContent : ""));
setTimeout(function () {
if (btn.dataset.confirming === "true") {
btn.dataset.confirming = "false";
btn.textContent = btn.dataset.label || "Pay now";
btn.classList.remove("paybtn--confirm");
}
}, 4000);
return;
}
// Confirmed
row.dataset.amount = "0";
row.classList.add("inv--paid");
var status = row.querySelector(".inv__status");
if (status) {
status.className = "badge badge--ok inv__status";
status.textContent = "Paid";
}
btn.classList.remove("paybtn--confirm");
btn.textContent = "Paid";
btn.disabled = true;
recalcBalance();
announce("Invoice paid. Outstanding balance updated.");
});
});
/* ---------- Mark message read ---------- */
document.querySelectorAll("[data-read]").forEach(function (btn) {
btn.addEventListener("click", function () {
var row = btn.closest(".msg");
if (!row) return;
row.removeAttribute("data-unread");
row.classList.remove("msg--unread");
row.classList.add("is-read");
btn.textContent = "Read";
btn.disabled = true;
recalcMessages();
announce("Message marked as read.");
});
});
/* ---------- Acknowledge document ---------- */
document.querySelectorAll("[data-ack]").forEach(function (btn) {
btn.addEventListener("click", function () {
var row = btn.closest(".doc");
if (!row) return;
var status = row.querySelector(".doc__status");
if (status) {
status.className = "badge badge--ok doc__status";
status.textContent = "Signed";
}
btn.textContent = "Acknowledged";
btn.disabled = true;
announce("Document acknowledged.");
});
});
/* ---------- Notifications button focuses messages ---------- */
var notifBtn = notifDot ? notifDot.closest(".iconbtn") : null;
if (notifBtn) {
notifBtn.addEventListener("click", function () {
activate("messages");
if (panes.messages) panes.messages.scrollIntoView({ behavior: "smooth", block: "start" });
});
}
// Initial sync
recalcBalance();
recalcMessages();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hartwell & Voss — Client Portal</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="portal">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">H&V</span>
<span class="brand__name">
<strong>Hartwell & Voss</strong>
<small>Attorneys at Law</small>
</span>
</div>
<div class="topbar__right">
<button class="iconbtn" type="button" aria-label="Notifications">
<span class="iconbtn__dot" id="notifDot"></span>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 22a2.4 2.4 0 0 0 2.4-2.4H9.6A2.4 2.4 0 0 0 12 22Zm6.6-5.6V11a6.6 6.6 0 0 0-5-6.4V4a1.6 1.6 0 0 0-3.2 0v.6A6.6 6.6 0 0 0 5.4 11v5.4L3.6 18.2v.8h16.8v-.8Z"/></svg>
</button>
<div class="who">
<span class="who__avatar" aria-hidden="true">EM</span>
<span class="who__meta">
<strong>Eleanor Marsh</strong>
<small>Client · since 2021</small>
</span>
</div>
</div>
</header>
<main class="main" id="main">
<!-- Greeting -->
<section class="greeting" aria-labelledby="greetHead">
<p class="greeting__eyebrow">Client Portal</p>
<h1 class="greeting__title" id="greetHead">Good afternoon, Eleanor.</h1>
<p class="greeting__sub">
Here is the latest on your matters with Hartwell & Voss. Your dedicated attorney is
<strong>Priya Nandakumar</strong>.
</p>
</section>
<!-- Summary cards -->
<section class="summary" aria-label="Account summary">
<article class="stat">
<p class="stat__label">Active matters</p>
<p class="stat__value" id="statMatters">3</p>
<p class="stat__hint">1 awaiting your action</p>
</article>
<article class="stat">
<p class="stat__label">Next appointment</p>
<p class="stat__value stat__value--sm">Jul 2, 10:30 AM</p>
<p class="stat__hint">Strategy call · video</p>
</article>
<article class="stat stat--alert">
<p class="stat__label">Outstanding balance</p>
<p class="stat__value" id="statBalance">$4,820.00</p>
<p class="stat__hint" id="statBalanceHint">2 invoices due</p>
</article>
<article class="stat">
<p class="stat__label">Unread messages</p>
<p class="stat__value" id="statMessages">2</p>
<p class="stat__hint">from your legal team</p>
</article>
</section>
<!-- Quick actions -->
<section class="quick" aria-label="Quick actions">
<button class="quick__btn" type="button" data-go="invoices">Pay an invoice</button>
<button class="quick__btn" type="button" data-go="documents">Review documents</button>
<button class="quick__btn" type="button" data-go="messages">Message the firm</button>
<button class="quick__btn quick__btn--ghost" type="button">Book a consultation</button>
</section>
<!-- Tabs -->
<section class="panel" aria-label="Portal sections">
<div class="tabs" role="tablist" aria-label="Client portal sections">
<button class="tab is-active" role="tab" id="tab-matters" aria-selected="true" aria-controls="pane-matters" data-tab="matters">
Matters <span class="tab__count">3</span>
</button>
<button class="tab" role="tab" id="tab-documents" aria-selected="false" aria-controls="pane-documents" data-tab="documents" tabindex="-1">
Documents <span class="tab__count">4</span>
</button>
<button class="tab" role="tab" id="tab-invoices" aria-selected="false" aria-controls="pane-invoices" data-tab="invoices" tabindex="-1">
Invoices <span class="tab__count" id="invCount">3</span>
</button>
<button class="tab" role="tab" id="tab-messages" aria-selected="false" aria-controls="pane-messages" data-tab="messages" tabindex="-1">
Messages <span class="tab__count" id="msgCount">2</span>
</button>
</div>
<!-- Matters -->
<div class="pane is-active" id="pane-matters" role="tabpanel" aria-labelledby="tab-matters" tabindex="0">
<ul class="list" aria-label="Active matters">
<li class="matter">
<div class="matter__head">
<h3 class="matter__title">Estate of Harold Marsh — Probate</h3>
<span class="badge badge--active">Active</span>
</div>
<p class="matter__ref">Matter #PB-2024-0417 · Probate & Estates</p>
<dl class="matter__meta">
<div><dt>Lead attorney</dt><dd>Priya Nandakumar</dd></div>
<div><dt>Opened</dt><dd>Apr 17, 2024</dd></div>
<div><dt>Next step</dt><dd>File inventory by Jul 9</dd></div>
</dl>
</li>
<li class="matter">
<div class="matter__head">
<h3 class="matter__title">Marsh v. Cedar Ridge HOA</h3>
<span class="badge badge--action">Needs your action</span>
</div>
<p class="matter__ref">Matter #LIT-2025-0083 · Civil Litigation</p>
<dl class="matter__meta">
<div><dt>Lead attorney</dt><dd>Daniel Okafor</dd></div>
<div><dt>Opened</dt><dd>Feb 3, 2025</dd></div>
<div><dt>Next step</dt><dd>Sign engagement addendum</dd></div>
</dl>
</li>
<li class="matter">
<div class="matter__head">
<h3 class="matter__title">Lakeside Cottage — Title Transfer</h3>
<span class="badge badge--hold">On hold</span>
</div>
<p class="matter__ref">Matter #RE-2025-0211 · Real Estate</p>
<dl class="matter__meta">
<div><dt>Lead attorney</dt><dd>Priya Nandakumar</dd></div>
<div><dt>Opened</dt><dd>May 28, 2025</dd></div>
<div><dt>Next step</dt><dd>Awaiting survey from county</dd></div>
</dl>
</li>
</ul>
<h2 class="subhead">Upcoming appointments</h2>
<ul class="list" aria-label="Upcoming appointments">
<li class="appt">
<div class="appt__date" aria-hidden="true"><span>JUL</span><strong>02</strong></div>
<div class="appt__body">
<h3 class="appt__title">Probate strategy call</h3>
<p class="appt__meta">10:30 AM · 30 min · with Priya Nandakumar</p>
<span class="badge badge--video">Video · Zoom link to follow</span>
</div>
</li>
<li class="appt">
<div class="appt__date" aria-hidden="true"><span>JUL</span><strong>15</strong></div>
<div class="appt__body">
<h3 class="appt__title">Document signing — HOA addendum</h3>
<p class="appt__meta">2:00 PM · 45 min · with Daniel Okafor</p>
<span class="badge badge--inperson">In person · Suite 1400</span>
</div>
</li>
</ul>
</div>
<!-- Documents -->
<div class="pane" id="pane-documents" role="tabpanel" aria-labelledby="tab-documents" tabindex="0" hidden>
<ul class="list" id="docList" aria-label="Recent documents">
<li class="doc" data-doc="engagement">
<span class="doc__icon" aria-hidden="true">PDF</span>
<div class="doc__body">
<h3 class="doc__title">Engagement Addendum — Marsh v. Cedar Ridge HOA</h3>
<p class="doc__meta">Shared Jun 19, 2026 · 4 pages · 218 KB</p>
</div>
<span class="badge badge--action doc__status">Needs signature</span>
<button class="linkbtn" type="button" data-ack>Acknowledge</button>
</li>
<li class="doc">
<span class="doc__icon" aria-hidden="true">PDF</span>
<div class="doc__body">
<h3 class="doc__title">Estate Inventory — Draft v2</h3>
<p class="doc__meta">Shared Jun 14, 2026 · 11 pages · 642 KB</p>
</div>
<span class="badge badge--neutral doc__status">For review</span>
<button class="linkbtn" type="button" data-ack>Acknowledge</button>
</li>
<li class="doc">
<span class="doc__icon" aria-hidden="true">DOC</span>
<div class="doc__body">
<h3 class="doc__title">Settlement Demand Letter</h3>
<p class="doc__meta">Shared Jun 6, 2026 · 3 pages · 96 KB</p>
</div>
<span class="badge badge--ok doc__status">Signed</span>
<button class="linkbtn" type="button" disabled>Acknowledged</button>
</li>
<li class="doc">
<span class="doc__icon" aria-hidden="true">PDF</span>
<div class="doc__body">
<h3 class="doc__title">Retainer Agreement — Probate</h3>
<p class="doc__meta">Shared Apr 17, 2024 · 8 pages · 405 KB</p>
</div>
<span class="badge badge--ok doc__status">Signed</span>
<button class="linkbtn" type="button" disabled>Acknowledged</button>
</li>
</ul>
</div>
<!-- Invoices -->
<div class="pane" id="pane-invoices" role="tabpanel" aria-labelledby="tab-invoices" tabindex="0" hidden>
<ul class="list" id="invList" aria-label="Invoices">
<li class="inv" data-amount="2940">
<div class="inv__body">
<h3 class="inv__title">Invoice #INV-2026-0148</h3>
<p class="inv__meta">Probate · Issued Jun 1 · Due Jul 1, 2026</p>
</div>
<span class="inv__amount">$2,940.00</span>
<span class="badge badge--overdue inv__status">Overdue</span>
<button class="paybtn" type="button" data-pay>Pay now</button>
</li>
<li class="inv" data-amount="1880">
<div class="inv__body">
<h3 class="inv__title">Invoice #INV-2026-0163</h3>
<p class="inv__meta">Civil Litigation · Issued Jun 12 · Due Jul 12, 2026</p>
</div>
<span class="inv__amount">$1,880.00</span>
<span class="badge badge--neutral inv__status">Due soon</span>
<button class="paybtn" type="button" data-pay>Pay now</button>
</li>
<li class="inv inv--paid" data-amount="0">
<div class="inv__body">
<h3 class="inv__title">Invoice #INV-2026-0102</h3>
<p class="inv__meta">Real Estate · Issued May 1 · Paid May 19, 2026</p>
</div>
<span class="inv__amount">$1,250.00</span>
<span class="badge badge--ok inv__status">Paid</span>
<button class="paybtn" type="button" disabled>Paid</button>
</li>
</ul>
</div>
<!-- Messages -->
<div class="pane" id="pane-messages" role="tabpanel" aria-labelledby="tab-messages" tabindex="0" hidden>
<ul class="list" id="msgList" aria-label="Messages from the firm">
<li class="msg msg--unread" data-unread>
<span class="msg__avatar" aria-hidden="true">PN</span>
<div class="msg__body">
<div class="msg__head">
<h3 class="msg__from">Priya Nandakumar</h3>
<time class="msg__time">Today, 9:14 AM</time>
</div>
<p class="msg__text">The county confirmed the probate hearing date. Let's review our filing on our Jul 2 call — no action needed before then.</p>
<span class="badge badge--action msg__flag">Unread</span>
</div>
<button class="linkbtn" type="button" data-read>Mark read</button>
</li>
<li class="msg msg--unread" data-unread>
<span class="msg__avatar" aria-hidden="true">DO</span>
<div class="msg__body">
<div class="msg__head">
<h3 class="msg__from">Daniel Okafor</h3>
<time class="msg__time">Yesterday, 4:48 PM</time>
</div>
<p class="msg__text">Please sign the engagement addendum in your Documents tab so we can proceed with the HOA matter.</p>
<span class="badge badge--action msg__flag">Unread</span>
</div>
<button class="linkbtn" type="button" data-read>Mark read</button>
</li>
<li class="msg">
<span class="msg__avatar" aria-hidden="true">BL</span>
<div class="msg__body">
<div class="msg__head">
<h3 class="msg__from">Billing — Hartwell & Voss</h3>
<time class="msg__time">Jun 12, 2026</time>
</div>
<p class="msg__text">Your June invoice for the litigation matter is now available in the Invoices tab.</p>
</div>
<button class="linkbtn" type="button" disabled>Read</button>
</li>
</ul>
</div>
</section>
<p class="disclaimer" role="note">
Illustrative UI only — not for real legal use or advice.
</p>
</main>
</div>
<!-- Live region for announcements -->
<p class="sr-only" role="status" aria-live="polite" id="live"></p>
<script src="script.js"></script>
</body>
</html>Client Portal
An authoritative, trust-first client portal for a law firm. The dashboard opens with a personal greeting and four summary cards — active matters, next appointment, outstanding balance and unread messages — set against warm paper and thin gold rules. A serif heading scale and Inter body text give it the settled, professional weight clients expect from counsel.
Below the summary, a segmented tab control switches between Matters, Documents, Invoices and Messages, each pane filled with realistic placeholder data: matter numbers, attorney names, document types, invoice amounts and dated messages from the firm. Status badges — Active, On hold, Overdue, Paid, Signed — communicate state at a glance.
Quick actions let the client pay an invoice (with an inline confirmation), mark messages as read, and acknowledge documents. Every interaction updates the live counts in the summary cards. All behaviour is vanilla JavaScript with no libraries, accessible headings and labels, focus styles, and a responsive layout that collapses cleanly on small screens.
Illustrative UI only — not for real legal use or advice.