Banking — Customer Detail
A trust-first admin customer detail screen for a fintech back office. It pairs a profile header carrying an animated risk score, verified-identity cues and an action toolbar with tabbed sections for overview, accounts, activity and internal notes. Operators can freeze accounts, flag the customer for review, re-run KYC and add notes, with toast feedback and a live-updating risk ring. Built with semantic HTML, layered shadows, status pills and tabular figures for every monetary value.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.10);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 1px 3px rgba(14, 27, 58, 0.05);
--sh-2: 0 6px 20px rgba(14, 27, 58, 0.08), 0 2px 6px rgba(14, 27, 58, 0.05);
--sh-3: 0 18px 50px rgba(14, 27, 58, 0.16);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-variant-numeric: tabular-nums;
}
.money, .mono { font-variant-numeric: tabular-nums; }
.mono { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
h1, h2, h3 { margin: 0; line-height: 1.25; }
button { font-family: inherit; cursor: pointer; }
.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;
}
.shell { min-height: 100vh; }
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 20px;
padding: 14px 22px;
background: var(--navy);
background: linear-gradient(180deg, var(--navy), var(--navy-2));
color: #fff;
position: sticky;
top: 0;
z-index: 20;
}
.brand { display: flex; align-items: center; gap: 9px; }
.brand-mark {
display: grid; place-items: center;
width: 28px; height: 28px;
background: linear-gradient(135deg, var(--accent), var(--violet));
border-radius: 9px; font-size: 16px;
}
.brand-name { font-weight: 800; font-size: 16px; letter-spacing: -0.01em; }
.brand-name span { color: var(--teal); }
.env-pill {
margin-left: 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
padding: 3px 8px; border-radius: 999px;
background: rgba(255, 255, 255, 0.12); color: #cfe0ff;
}
.crumbs {
display: flex; align-items: center; gap: 8px;
font-size: 13px; color: rgba(255, 255, 255, 0.6);
}
.crumbs a { color: rgba(255, 255, 255, 0.72); text-decoration: none; }
.crumbs a:hover { color: #fff; }
.crumb-current { color: #fff; font-weight: 600; }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
.icon-btn {
width: 34px; height: 34px; border: 0; border-radius: 10px;
background: rgba(255, 255, 255, 0.10); color: #fff; font-size: 17px;
transition: background 0.15s;
}
.icon-btn:hover { background: rgba(255, 255, 255, 0.20); }
.op-badge { display: flex; align-items: center; gap: 9px; }
.op-avatar {
display: grid; place-items: center; width: 32px; height: 32px;
border-radius: 50%; background: var(--accent); color: #fff;
font-size: 12px; font-weight: 700;
}
.op-meta { display: flex; flex-direction: column; line-height: 1.2; }
.op-meta strong { font-size: 13px; }
.op-meta em { font-style: normal; font-size: 11px; color: rgba(255, 255, 255, 0.6); }
/* ---------- Content ---------- */
.content {
max-width: 1080px;
margin: 0 auto;
padding: 26px 22px 60px;
display: flex;
flex-direction: column;
gap: 20px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 20px;
}
/* ---------- Profile ---------- */
.profile {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "main side" "toolbar toolbar";
gap: 20px 24px;
padding: 24px;
}
.profile-main { grid-area: main; display: flex; gap: 18px; }
.profile-side { grid-area: side; }
.toolbar { grid-area: toolbar; }
.avatar {
flex: none;
width: 72px; height: 72px; border-radius: 18px;
display: grid; place-items: center;
background: linear-gradient(135deg, var(--navy-2), var(--accent));
color: #fff; font-size: 24px; font-weight: 700;
box-shadow: var(--sh-2);
}
.name-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.name-row h1 { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.verify-badge {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 600; color: var(--ok);
background: rgba(31, 157, 98, 0.10); border: 1px solid rgba(31, 157, 98, 0.22);
padding: 3px 9px; border-radius: 999px;
}
.verify-badge .lock { font-size: 11px; }
.meta-list {
list-style: none; margin: 12px 0 0; padding: 0;
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 24px; max-width: 440px;
}
.meta-list li { display: flex; flex-direction: column; gap: 1px; }
.meta-list .k { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
.meta-list .v { font-size: 14px; font-weight: 600; color: var(--ink); }
/* status pills */
.status-pill {
display: inline-flex; align-items: center; gap: 5px;
font-size: 11.5px; font-weight: 700; letter-spacing: 0.02em;
padding: 3px 10px; border-radius: 999px;
border: 1px solid transparent;
}
.status-pill[data-state="active"], .status-pill[data-state="cleared"], .status-pill[data-state="verified"] {
color: var(--ok); background: rgba(31, 157, 98, 0.10); border-color: rgba(31, 157, 98, 0.20);
}
.status-pill[data-state="pending"] {
color: var(--warn); background: rgba(217, 152, 43, 0.12); border-color: rgba(217, 152, 43, 0.24);
}
.status-pill[data-state="failed"], .status-pill[data-state="frozen"] {
color: var(--danger); background: rgba(212, 73, 62, 0.10); border-color: rgba(212, 73, 62, 0.22);
}
.status-pill[data-state="closed"] {
color: var(--muted); background: rgba(14, 27, 58, 0.06); border-color: var(--line);
}
.status-pill[data-state="active"]::before, .status-pill[data-state="cleared"]::before,
.status-pill[data-state="verified"]::before, .status-pill[data-state="pending"]::before,
.status-pill[data-state="failed"]::before, .status-pill[data-state="frozen"]::before {
content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor;
}
/* ---------- Risk widget ---------- */
.risk { display: flex; align-items: center; gap: 14px; }
.risk-ring { position: relative; width: 96px; height: 96px; }
.risk-ring svg { width: 96px; height: 96px; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--line); stroke-width: 10; }
.ring-fg {
fill: none; stroke: var(--ok); stroke-width: 10; stroke-linecap: round;
stroke-dasharray: 327; stroke-dashoffset: 327;
transition: stroke-dashoffset 0.9s cubic-bezier(0.16, 1, 0.3, 1), stroke 0.4s;
}
.risk[data-level="medium"] .ring-fg { stroke: var(--warn); }
.risk[data-level="high"] .ring-fg { stroke: var(--danger); }
.risk-num {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; line-height: 1;
}
.risk-num strong { font-size: 24px; font-weight: 800; }
.risk-num span { font-size: 11px; color: var(--muted); }
.risk-label { display: flex; flex-direction: column; gap: 3px; }
.risk-tag {
font-size: 13px; font-weight: 700; color: var(--ok);
}
.risk[data-level="medium"] .risk-tag { color: var(--warn); }
.risk[data-level="high"] .risk-tag { color: var(--danger); }
.risk-label small { font-size: 11px; color: var(--muted); }
/* ---------- Toolbar buttons ---------- */
.toolbar {
display: flex; gap: 10px; flex-wrap: wrap;
padding-top: 18px; border-top: 1px solid var(--line);
}
.btn {
display: inline-flex; align-items: center; gap: 7px;
font-size: 13.5px; font-weight: 600;
padding: 9px 15px; border-radius: var(--r-sm);
border: 1px solid var(--line-2); background: var(--surface); color: var(--ink);
transition: transform 0.08s, box-shadow 0.15s, background 0.15s, border-color 0.15s;
}
.btn:hover { border-color: var(--accent); box-shadow: var(--sh-1); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-d); border-color: var(--accent-d); }
.btn-warn { color: var(--warn); border-color: rgba(217, 152, 43, 0.35); }
.btn-warn:hover { background: rgba(217, 152, 43, 0.08); border-color: var(--warn); }
.btn-danger { color: var(--danger); border-color: rgba(212, 73, 62, 0.35); }
.btn-danger:hover { background: rgba(212, 73, 62, 0.07); border-color: var(--danger); }
.btn-ghost { background: transparent; border-color: var(--line); color: var(--ink-2); }
.btn[aria-pressed="true"] { background: currentColor; }
.btn-warn[aria-pressed="true"] { background: var(--warn); color: #fff; border-color: var(--warn); }
.btn-danger[aria-pressed="true"] { background: var(--danger); color: #fff; border-color: var(--danger); }
.link-btn {
border: 0; background: none; color: var(--accent); font-weight: 600;
font-size: 13px; padding: 0;
}
.link-btn:hover { color: var(--accent-d); text-decoration: underline; }
/* ---------- Tabs ---------- */
.tabs {
display: flex; gap: 4px;
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-md); padding: 5px;
box-shadow: var(--sh-1);
}
.tab {
flex: 1; border: 0; background: none; color: var(--muted);
font-size: 13.5px; font-weight: 600; padding: 9px 12px;
border-radius: var(--r-sm); transition: background 0.15s, color 0.15s;
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
}
.tab:hover { color: var(--ink); background: rgba(14, 27, 58, 0.04); }
.tab.is-active { background: var(--accent-50); color: var(--accent-d); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tab-count {
font-size: 11px; font-weight: 700; min-width: 18px; height: 18px;
padding: 0 5px; border-radius: 999px; background: var(--accent); color: #fff;
display: inline-grid; place-items: center;
}
/* ---------- Panels ---------- */
.panel { animation: fade 0.25s ease; }
.panel[hidden] { display: none; }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 18px; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.stat { display: flex; flex-direction: column; gap: 6px; }
.stat-k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.stat-v { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
.stat-trend { font-size: 12px; color: var(--muted); }
.stat-trend.up { color: var(--ok); font-weight: 600; }
.stat-trend.warn { color: var(--warn); font-weight: 600; }
.card-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; margin-bottom: 14px;
}
.card-head h2 { font-size: 15px; font-weight: 700; }
/* KYC list */
.kyc-list, .contact-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.kyc-list li, .contact-list li {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--line);
}
.kyc-list li:last-child, .contact-list li:last-child { border-bottom: 0; }
.kyc-k, .c-k { font-size: 13px; color: var(--ink-2); }
.chip {
font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
}
.chip.ok { color: var(--ok); background: rgba(31, 157, 98, 0.10); }
.chip.warn { color: var(--warn); background: rgba(217, 152, 43, 0.12); }
.c-v { font-size: 13px; font-weight: 600; text-align: right; }
.verify-inline {
font-style: normal; font-size: 10.5px; font-weight: 700; color: var(--ok);
background: rgba(31, 157, 98, 0.10); padding: 1px 6px; border-radius: 999px; margin-left: 4px;
}
/* ---------- Accounts ---------- */
.account-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.account {
display: flex; flex-direction: column; gap: 10px;
position: relative; overflow: hidden;
transition: transform 0.12s, box-shadow 0.15s;
}
.account:hover { transform: translateY(-2px); box-shadow: var(--sh-2); }
.account::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
background: linear-gradient(180deg, var(--accent), var(--violet));
}
.account.is-closed { opacity: 0.72; }
.account.is-closed::before { background: var(--line-2); }
.acc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.acc-type { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.acc-name { font-size: 16px; font-weight: 700; margin-top: 2px; }
.acc-num { font-size: 13px; color: var(--ink-2); letter-spacing: 0.06em; }
.acc-balance { font-size: 24px; font-weight: 800; letter-spacing: -0.02em; }
.acc-foot {
display: flex; justify-content: space-between; gap: 8px;
font-size: 11.5px; color: var(--muted); padding-top: 8px; border-top: 1px solid var(--line);
}
/* ---------- Activity ---------- */
.filter-row { display: flex; gap: 6px; flex-wrap: wrap; }
.filter {
font-size: 12px; font-weight: 600; padding: 5px 11px;
border: 1px solid var(--line); background: var(--surface); color: var(--muted);
border-radius: 999px; transition: all 0.13s;
}
.filter:hover { color: var(--ink); border-color: var(--line-2); }
.filter.is-active { background: var(--ink); color: #fff; border-color: var(--ink); }
.txn-list { list-style: none; margin: 0; padding: 0; }
.txn {
display: grid;
grid-template-columns: 36px 1fr auto auto;
align-items: center; gap: 14px;
padding: 12px 0; border-bottom: 1px solid var(--line);
transition: background 0.13s;
}
.txn:last-child { border-bottom: 0; }
.txn:hover { background: rgba(59, 110, 246, 0.03); }
.txn-icon {
width: 36px; height: 36px; border-radius: 11px;
display: grid; place-items: center; font-size: 15px; font-weight: 700;
}
.txn-icon.credit { background: rgba(31, 157, 98, 0.12); color: var(--ok); }
.txn-icon.debit { background: rgba(14, 27, 58, 0.06); color: var(--ink); }
.txn-icon.security { background: rgba(124, 92, 255, 0.12); color: var(--violet); }
.txn-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.txn-main strong { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.txn-main small { font-size: 12px; color: var(--muted); }
.txn-amt { font-size: 14.5px; font-weight: 700; text-align: right; white-space: nowrap; }
.txn-amt.credit { color: var(--credit); }
.txn-amt.muted { color: var(--muted); font-weight: 500; }
/* ---------- Notes ---------- */
.note-form { display: flex; gap: 10px; margin-bottom: 16px; align-items: flex-end; }
.note-form textarea {
flex: 1; resize: vertical; min-height: 44px;
font-family: inherit; font-size: 13.5px; color: var(--ink);
border: 1px solid var(--line-2); border-radius: var(--r-sm); padding: 10px 12px;
background: var(--bg);
}
.note-form textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
.note-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.note {
background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 12px 14px;
}
.note-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 4px; }
.note-head strong { font-size: 13px; }
.note-head time { font-size: 11.5px; color: var(--muted); }
.note p { margin: 0; font-size: 13px; color: var(--ink-2); }
.note.is-new { animation: noteIn 0.3s ease; }
@keyframes noteIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed; right: 20px; bottom: 20px; z-index: 60;
display: flex; flex-direction: column; gap: 10px; max-width: calc(100vw - 40px);
}
.toast {
display: flex; align-items: center; gap: 10px;
background: var(--ink); color: #fff;
padding: 12px 16px; border-radius: var(--r-md);
box-shadow: var(--sh-3); font-size: 13.5px; font-weight: 500;
animation: toastIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.toast.out { animation: toastOut 0.25s ease forwards; }
.toast-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--teal); flex: none; }
.toast.warn .toast-dot { background: var(--warn); }
.toast.danger .toast-dot { background: var(--danger); }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(8px); } }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.grid-3, .grid-2, .account-grid { grid-template-columns: 1fr; }
.profile { grid-template-columns: 1fr; grid-template-areas: "main" "side" "toolbar"; }
.crumbs { display: none; }
}
@media (max-width: 520px) {
.topbar { gap: 12px; padding: 12px 16px; }
.op-meta { display: none; }
.content { padding: 18px 14px 48px; }
.card, .profile { padding: 16px; border-radius: var(--r-md); }
.profile-main { gap: 14px; }
.avatar { width: 56px; height: 56px; font-size: 19px; border-radius: 14px; }
.name-row h1 { font-size: 19px; }
.meta-list { grid-template-columns: 1fr 1fr; gap: 8px 16px; }
.tabs { overflow-x: auto; }
.tab { flex: none; padding: 8px 12px; }
.stat-v { font-size: 22px; }
.txn { grid-template-columns: 32px 1fr auto; gap: 10px; }
.txn-tag { display: none; }
.txn-icon { width: 32px; height: 32px; }
.note-form { flex-direction: column; align-items: stretch; }
.note-form .btn { justify-content: center; }
.toast-wrap { left: 14px; right: 14px; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg, variant) {
var el = document.createElement("div");
el.className = "toast" + (variant ? " " + variant : "");
el.setAttribute("role", "status");
var dot = document.createElement("span");
dot.className = "toast-dot";
var text = document.createElement("span");
text.textContent = msg;
el.appendChild(dot);
el.appendChild(text);
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
el.addEventListener("animationend", function () { el.remove(); });
}, 3200);
}
/* ---------- Tabs ---------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".tab"));
function selectTab(tab) {
tabs.forEach(function (t) {
var active = t === tab;
t.classList.toggle("is-active", active);
t.setAttribute("aria-selected", active ? "true" : "false");
t.tabIndex = active ? 0 : -1;
var panel = document.getElementById(t.getAttribute("aria-controls"));
if (panel) {
panel.classList.toggle("is-active", active);
panel.hidden = !active;
}
});
}
tabs.forEach(function (tab, i) {
tab.addEventListener("click", function () { selectTab(tab); });
tab.addEventListener("keydown", function (e) {
var dir = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!dir) return;
e.preventDefault();
var next = tabs[(i + dir + tabs.length) % tabs.length];
next.focus();
selectTab(next);
});
});
/* ---------- Risk ring fill ---------- */
var ring = document.getElementById("riskRing");
var scoreEl = document.getElementById("riskScore");
if (ring && scoreEl) {
var score = parseInt(scoreEl.textContent, 10) || 0;
var circ = 2 * Math.PI * 52; // 326.7
requestAnimationFrame(function () {
ring.style.strokeDashoffset = String(circ * (1 - score / 100));
});
}
/* ---------- Action toolbar ---------- */
var statePill = document.getElementById("acctStatePill");
var riskWidget = document.getElementById("riskWidget");
var riskTag = document.getElementById("riskTag");
function handleAction(btn) {
var action = btn.getAttribute("data-action");
if (action === "message") {
// jump to notes tab as a quick compose surface
var notesTab = document.getElementById("tab-notes");
if (notesTab) { selectTab(notesTab); notesTab.focus(); }
toast("Compose a message to Amara Okonkwo");
return;
}
if (action === "kyc") {
toast("KYC re-run queued — results in ~2 min");
return;
}
if (action === "flag") {
var flagged = btn.getAttribute("aria-pressed") === "true";
var now = !flagged;
btn.setAttribute("aria-pressed", now ? "true" : "false");
btn.querySelector("span:last-child") || null;
btn.lastChild.textContent = now ? " Flagged" : " Flag review";
if (now) {
bumpRisk(true);
toast("Customer flagged for manual review", "warn");
} else {
bumpRisk(false);
toast("Review flag cleared");
}
return;
}
if (action === "freeze") {
var frozen = btn.getAttribute("aria-pressed") === "true";
var nowF = !frozen;
btn.setAttribute("aria-pressed", nowF ? "true" : "false");
btn.lastChild.textContent = nowF ? " Unfreeze account" : " Freeze account";
if (statePill) {
statePill.setAttribute("data-state", nowF ? "frozen" : "active");
statePill.textContent = nowF ? "Frozen" : "Active";
}
// reflect on account cards
document.querySelectorAll(".account:not(.is-closed) .status-pill").forEach(function (p) {
p.setAttribute("data-state", nowF ? "frozen" : "active");
p.textContent = nowF ? "Frozen" : "Active";
});
toast(nowF ? "All accounts frozen — outbound payments blocked" : "Accounts unfrozen", nowF ? "danger" : null);
return;
}
}
// risk escalation when flagged
var baseScore = 24;
function bumpRisk(up) {
if (!ring || !scoreEl || !riskWidget) return;
var s = up ? 61 : baseScore;
scoreEl.textContent = String(s);
var c = 2 * Math.PI * 52;
ring.style.strokeDashoffset = String(c * (1 - s / 100));
var level = s >= 60 ? "high" : s >= 35 ? "medium" : "low";
riskWidget.setAttribute("data-level", level);
if (riskTag) {
riskTag.textContent = level === "high" ? "Elevated risk" : level === "medium" ? "Watch" : "Low risk";
}
}
document.querySelectorAll("[data-action]").forEach(function (btn) {
btn.addEventListener("click", function () { handleAction(btn); });
});
/* ---------- Activity filters ---------- */
var filters = document.querySelectorAll(".filter");
var txns = Array.prototype.slice.call(document.querySelectorAll("#txnList .txn"));
filters.forEach(function (f) {
f.addEventListener("click", function () {
filters.forEach(function (x) { x.classList.remove("is-active"); });
f.classList.add("is-active");
var kind = f.getAttribute("data-filter");
var shown = 0;
txns.forEach(function (t) {
var match = kind === "all" || t.getAttribute("data-kind") === kind;
t.style.display = match ? "" : "none";
if (match) shown++;
});
toast(shown + (shown === 1 ? " entry" : " entries") + " shown");
});
});
/* ---------- Notes ---------- */
var noteForm = document.getElementById("noteForm");
var noteInput = document.getElementById("noteInput");
var noteList = document.getElementById("noteList");
var noteCount = document.getElementById("noteCount");
if (noteForm) {
noteForm.addEventListener("submit", function (e) {
e.preventDefault();
var val = noteInput.value.trim();
if (!val) { toast("Note is empty", "warn"); noteInput.focus(); return; }
var li = document.createElement("li");
li.className = "note is-new";
var head = document.createElement("div");
head.className = "note-head";
var who = document.createElement("strong");
who.textContent = "L. Brandt";
var when = document.createElement("time");
when.textContent = "Just now";
head.appendChild(who);
head.appendChild(when);
var p = document.createElement("p");
p.textContent = val;
li.appendChild(head);
li.appendChild(p);
noteList.insertBefore(li, noteList.firstChild);
noteInput.value = "";
if (noteCount) noteCount.textContent = String(noteList.querySelectorAll(".note").length);
toast("Note added to customer record");
});
}
/* ---------- Search (decorative) ---------- */
var searchBtn = document.getElementById("searchBtn");
if (searchBtn) {
searchBtn.addEventListener("click", function () {
toast("Search is disabled in this sandbox demo");
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Customer Detail</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<!-- Topbar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<span class="brand-name">Meridian<span>Ops</span></span>
<span class="env-pill" title="Sandbox environment">SANDBOX</span>
</div>
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">Customers</a>
<span aria-hidden="true">/</span>
<a href="#">All</a>
<span aria-hidden="true">/</span>
<span class="crumb-current">Amara Okonkwo</span>
</nav>
<div class="topbar-right">
<button class="icon-btn" id="searchBtn" aria-label="Search">⌕</button>
<div class="op-badge" title="Signed in as operator">
<span class="op-avatar">OP</span>
<span class="op-meta">
<strong>L. Brandt</strong>
<em>Fraud Ops · L2</em>
</span>
</div>
</div>
</header>
<main class="content">
<!-- Profile header -->
<section class="profile card" aria-label="Customer profile">
<div class="profile-main">
<div class="avatar" aria-hidden="true">AO</div>
<div class="profile-id">
<div class="name-row">
<h1>Amara Okonkwo</h1>
<span class="verify-badge" title="Identity verified">
<span class="lock" aria-hidden="true">✔</span> Verified
</span>
<span class="status-pill" id="acctStatePill" data-state="active">Active</span>
</div>
<ul class="meta-list">
<li><span class="k">Customer ID</span><span class="v mono">CU-4827-1190</span></li>
<li><span class="k">Member since</span><span class="v">Mar 2019</span></li>
<li><span class="k">Segment</span><span class="v">Premier · Tier 2</span></li>
<li><span class="k">Location</span><span class="v">Lisbon, PT</span></li>
</ul>
</div>
</div>
<div class="profile-side">
<div class="risk" id="riskWidget" data-level="low">
<div class="risk-ring" role="img" aria-label="Risk score 24 of 100, low">
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle class="ring-bg" cx="60" cy="60" r="52" />
<circle class="ring-fg" id="riskRing" cx="60" cy="60" r="52" />
</svg>
<div class="risk-num"><strong id="riskScore">24</strong><span>/100</span></div>
</div>
<div class="risk-label">
<span class="risk-tag" id="riskTag">Low risk</span>
<small>Updated 2h ago · 9 signals</small>
</div>
</div>
</div>
<!-- Action toolbar -->
<div class="toolbar" role="toolbar" aria-label="Customer actions">
<button class="btn" data-action="message">
<span aria-hidden="true">✉</span> Message
</button>
<button class="btn btn-warn" data-action="flag" aria-pressed="false">
<span aria-hidden="true">⚑</span> Flag review
</button>
<button class="btn btn-danger" data-action="freeze" aria-pressed="false">
<span aria-hidden="true">⏸</span> Freeze account
</button>
<button class="btn btn-ghost" data-action="kyc">
<span aria-hidden="true">↻</span> Re-run KYC
</button>
</div>
</section>
<!-- Tabs -->
<nav class="tabs" role="tablist" aria-label="Customer sections">
<button class="tab is-active" role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true">Overview</button>
<button class="tab" role="tab" id="tab-accounts" aria-controls="panel-accounts" aria-selected="false" tabindex="-1">Accounts</button>
<button class="tab" role="tab" id="tab-activity" aria-controls="panel-activity" aria-selected="false" tabindex="-1">Activity</button>
<button class="tab" role="tab" id="tab-notes" aria-controls="panel-notes" aria-selected="false" tabindex="-1">Notes <span class="tab-count" id="noteCount">2</span></button>
</nav>
<!-- Overview -->
<section class="panel is-active" id="panel-overview" role="tabpanel" aria-labelledby="tab-overview" tabindex="0">
<div class="grid-3">
<div class="card stat">
<span class="stat-k">Total balance</span>
<strong class="stat-v money">€48,213.55</strong>
<span class="stat-trend up">▲ 4.1% this month</span>
</div>
<div class="card stat">
<span class="stat-k">Open accounts</span>
<strong class="stat-v">3</strong>
<span class="stat-trend">2 current · 1 savings</span>
</div>
<div class="card stat">
<span class="stat-k">Disputes (90d)</span>
<strong class="stat-v">1</strong>
<span class="stat-trend warn">1 pending resolution</span>
</div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head">
<h2>KYC & Compliance</h2>
<span class="status-pill" data-state="verified">Verified</span>
</div>
<ul class="kyc-list">
<li><span class="kyc-k">Identity document</span><span class="chip ok">Passport · matched</span></li>
<li><span class="kyc-k">Proof of address</span><span class="chip ok">Utility bill · 2026</span></li>
<li><span class="kyc-k">Liveness check</span><span class="chip ok">Passed</span></li>
<li><span class="kyc-k">PEP / sanctions</span><span class="chip ok">No match</span></li>
<li><span class="kyc-k">2FA enrolled</span><span class="chip ok">Authenticator app</span></li>
<li><span class="kyc-k">Source of funds</span><span class="chip warn">Review due Jul 2026</span></li>
</ul>
</div>
<div class="card">
<div class="card-head">
<h2>Contact</h2>
<button class="link-btn" data-action="message">Send message</button>
</div>
<ul class="contact-list">
<li><span class="c-k">Email</span><span class="c-v">a.okonkwo@fictomail.test <em class="verify-inline">verified</em></span></li>
<li><span class="c-k">Phone</span><span class="c-v mono">+351 •• ••• 7714</span></li>
<li><span class="c-k">Primary IBAN</span><span class="c-v mono">PT50 0002 •••• •••• 4831</span></li>
<li><span class="c-k">Last login</span><span class="c-v">Today, 08:14 · Lisbon</span></li>
<li><span class="c-k">Device</span><span class="c-v">iPhone · trusted</span></li>
</ul>
</div>
</div>
</section>
<!-- Accounts -->
<section class="panel" id="panel-accounts" role="tabpanel" aria-labelledby="tab-accounts" tabindex="0" hidden>
<div class="account-grid">
<article class="card account">
<div class="acc-top">
<div>
<span class="acc-type">Current account</span>
<h3 class="acc-name">Everyday EUR</h3>
</div>
<span class="status-pill" data-state="active">Active</span>
</div>
<div class="acc-num mono">•••• •••• •••• 4831</div>
<div class="acc-balance money">€12,940.18</div>
<div class="acc-foot"><span>IBAN PT50 …4831</span><span>Visa Debit</span></div>
</article>
<article class="card account">
<div class="acc-top">
<div>
<span class="acc-type">Current account</span>
<h3 class="acc-name">Travel USD</h3>
</div>
<span class="status-pill" data-state="active">Active</span>
</div>
<div class="acc-num mono">•••• •••• •••• 7702</div>
<div class="acc-balance money">$3,128.90</div>
<div class="acc-foot"><span>Multi-currency</span><span>Mastercard</span></div>
</article>
<article class="card account">
<div class="acc-top">
<div>
<span class="acc-type">Savings</span>
<h3 class="acc-name">Vault 4.1% APY</h3>
</div>
<span class="status-pill" data-state="active">Active</span>
</div>
<div class="acc-num mono">•••• •••• •••• 9015</div>
<div class="acc-balance money">€32,144.47</div>
<div class="acc-foot"><span>IBAN PT50 …9015</span><span>No card</span></div>
</article>
<article class="card account is-closed">
<div class="acc-top">
<div>
<span class="acc-type">Credit line</span>
<h3 class="acc-name">Flex Credit</h3>
</div>
<span class="status-pill" data-state="closed">Closed</span>
</div>
<div class="acc-num mono">•••• •••• •••• 3360</div>
<div class="acc-balance money">€0.00</div>
<div class="acc-foot"><span>Closed Feb 2025</span><span>Paid in full</span></div>
</article>
</div>
</section>
<!-- Activity -->
<section class="panel" id="panel-activity" role="tabpanel" aria-labelledby="tab-activity" tabindex="0" hidden>
<div class="card">
<div class="card-head">
<h2>Recent activity</h2>
<div class="filter-row" role="group" aria-label="Filter activity">
<button class="filter is-active" data-filter="all">All</button>
<button class="filter" data-filter="credit">Credits</button>
<button class="filter" data-filter="debit">Debits</button>
<button class="filter" data-filter="security">Security</button>
</div>
</div>
<ul class="txn-list" id="txnList">
<li class="txn" data-kind="credit">
<span class="txn-icon credit" aria-hidden="true">↓</span>
<span class="txn-main"><strong>Salary — Helios Studios</strong><small>Today · 06:02 · Cleared</small></span>
<span class="txn-tag"><span class="status-pill" data-state="cleared">Cleared</span></span>
<span class="txn-amt credit money">+€3,420.00</span>
</li>
<li class="txn" data-kind="debit">
<span class="txn-icon debit" aria-hidden="true">↑</span>
<span class="txn-main"><strong>Lumen Grocers</strong><small>Today · 08:41 · Card •••• 4831</small></span>
<span class="txn-tag"><span class="status-pill" data-state="cleared">Cleared</span></span>
<span class="txn-amt money">−€62.18</span>
</li>
<li class="txn" data-kind="security">
<span class="txn-icon security" aria-hidden="true">⚿</span>
<span class="txn-main"><strong>2FA challenge passed</strong><small>Today · 08:14 · iPhone · Lisbon</small></span>
<span class="txn-tag"><span class="status-pill" data-state="verified">Secure</span></span>
<span class="txn-amt muted">—</span>
</li>
<li class="txn" data-kind="debit">
<span class="txn-icon debit" aria-hidden="true">↑</span>
<span class="txn-main"><strong>Vela Airlines</strong><small>Yesterday · 19:22 · Card •••• 7702</small></span>
<span class="txn-tag"><span class="status-pill" data-state="pending">Pending</span></span>
<span class="txn-amt money">−$248.50</span>
</li>
<li class="txn" data-kind="credit">
<span class="txn-icon credit" aria-hidden="true">↓</span>
<span class="txn-main"><strong>Refund — Orbit Electronics</strong><small>Yesterday · 11:03 · Cleared</small></span>
<span class="txn-tag"><span class="status-pill" data-state="cleared">Cleared</span></span>
<span class="txn-amt credit money">+€129.99</span>
</li>
<li class="txn" data-kind="security">
<span class="txn-icon security" aria-hidden="true">⚿</span>
<span class="txn-main"><strong>New device blocked</strong><small>2 days ago · 23:47 · Unknown · Manila</small></span>
<span class="txn-tag"><span class="status-pill" data-state="failed">Blocked</span></span>
<span class="txn-amt muted">—</span>
</li>
<li class="txn" data-kind="debit">
<span class="txn-icon debit" aria-hidden="true">↑</span>
<span class="txn-main"><strong>Aurora Rent · standing order</strong><small>3 days ago · 09:00 · IBAN …4831</small></span>
<span class="txn-tag"><span class="status-pill" data-state="cleared">Cleared</span></span>
<span class="txn-amt money">−€1,180.00</span>
</li>
</ul>
</div>
</section>
<!-- Notes -->
<section class="panel" id="panel-notes" role="tabpanel" aria-labelledby="tab-notes" tabindex="0" hidden>
<div class="card">
<div class="card-head"><h2>Internal notes</h2></div>
<form class="note-form" id="noteForm">
<label class="sr-only" for="noteInput">Add a note</label>
<textarea id="noteInput" rows="2" placeholder="Add an internal note (visible to ops only)…"></textarea>
<button class="btn btn-primary" type="submit">Add note</button>
</form>
<ul class="note-list" id="noteList">
<li class="note">
<div class="note-head"><strong>L. Brandt</strong><time>Today · 08:20</time></div>
<p>Reviewed blocked Manila login. Customer confirmed travel was Lisbon only — device correctly blocked. No action.</p>
</li>
<li class="note">
<div class="note-head"><strong>M. Sato</strong><time>Jun 11 · 14:55</time></div>
<p>Source-of-funds review scheduled for Jul 2026. Customer notified by email.</p>
</li>
</ul>
</div>
</section>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Customer Detail
A single-screen admin view for the people behind the money. The profile header fuses identity (avatar, verified badge, customer ID, segment) with an animated risk ring that fills from zero on load and a clear account-status pill. Below it, an action toolbar lets a fraud operator message, flag, freeze or re-run KYC — each button is keyboard-usable and reports back through a toast helper.
Four tabs organise the rest. Overview surfaces balance, open accounts and disputes alongside a KYC checklist and verified contact details. Accounts lists every product as a card with masked numbers and right-aligned, tabular-figure balances. Activity is a filterable feed of credits, debits and security events with cleared / pending / blocked status pills. Notes is a live internal log you can append to.
Interactions are intentionally consequential: flagging the customer escalates the risk ring to a higher band, and freezing cascades a frozen state onto every open account card. Everything is vanilla HTML, CSS and JavaScript — no frameworks, no build step — and it collapses cleanly down to a ~360px mobile width.
Illustrative UI only — not real banking software or financial advice.