Law Firm — Fee Estimator
An interactive law-firm fee estimator letting clients pick a practice area and billing model, then tune hours, hourly rate, complexity and expected recovery to see a live estimated total with an itemized breakdown, retainer line and clear non-binding disclaimer.
MCP
Code
:root {
--navy: #1b2a4a;
--navy-d: #101b33;
--gold: #b08d57;
--gold-soft: #efe3cf;
--ink: #1a1f2b;
--muted: #5b6577;
--bg: #f5f3ee;
--white: #ffffff;
--line: #e4ded2;
--radius: 12px;
--serif: "Georgia", "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow: 0 18px 44px -28px rgba(16, 27, 51, 0.45);
}
* {
box-sizing: border-box;
margin: 0;
}
body {
font-family: var(--sans);
color: var(--ink);
background:
radial-gradient(circle at 12% -10%, rgba(176, 141, 87, 0.1), transparent 45%),
var(--bg);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
padding: 40px 20px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.estimator {
max-width: 880px;
margin: 0 auto;
}
.estimator__head {
text-align: center;
margin-bottom: 28px;
}
.estimator__firm {
font-family: var(--sans);
text-transform: uppercase;
letter-spacing: 0.22em;
font-size: 0.72rem;
font-weight: 600;
color: var(--gold);
}
.estimator__head h1 {
font-family: var(--serif);
font-size: 2.15rem;
font-weight: 600;
color: var(--navy);
margin: 6px 0 10px;
letter-spacing: -0.01em;
}
.estimator__lede {
color: var(--muted);
max-width: 44ch;
margin: 0 auto;
font-size: 0.95rem;
}
.rule {
display: block;
width: 56px;
height: 2px;
margin: 22px auto 0;
background: var(--gold);
}
.estimator__grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 22px;
align-items: start;
}
.panel {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.form {
padding: 26px;
display: grid;
gap: 22px;
}
.field {
border: 0;
padding: 0;
display: grid;
gap: 9px;
}
legend,
.field > label,
.field__row label {
font-weight: 600;
font-size: 0.83rem;
color: var(--navy);
letter-spacing: 0.01em;
padding: 0;
}
.field__row {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.field__row output {
font-family: var(--serif);
font-size: 0.95rem;
color: var(--gold);
font-weight: 600;
}
/* select */
.select select {
width: 100%;
font-family: var(--sans);
font-size: 0.95rem;
color: var(--ink);
padding: 12px 38px 12px 14px;
border: 1px solid var(--line);
border-radius: 9px;
background:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='none' stroke='%231b2a4a' stroke-width='2'><path d='M2 4l5 5 5-5'/></svg>")
no-repeat right 14px center;
background-color: #fbfaf6;
appearance: none;
cursor: pointer;
}
/* segmented */
.segmented {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
background: #f2efe7;
border: 1px solid var(--line);
border-radius: 10px;
padding: 4px;
}
.seg {
position: relative;
display: block;
text-align: center;
cursor: pointer;
}
.seg input {
position: absolute;
opacity: 0;
}
.seg span {
display: block;
padding: 9px 4px;
border-radius: 7px;
font-size: 0.83rem;
font-weight: 600;
color: var(--muted);
transition: all 0.18s ease;
}
.seg input:checked + span {
background: var(--navy);
color: var(--white);
box-shadow: 0 4px 12px -4px rgba(16, 27, 51, 0.5);
}
.seg input:focus-visible + span {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* range */
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 5px;
border-radius: 99px;
background: var(--gold-soft);
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--navy);
border: 3px solid var(--white);
box-shadow: 0 2px 7px rgba(16, 27, 51, 0.4);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--navy);
border: 3px solid var(--white);
}
input[type="range"]:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 4px;
}
/* money input */
.money {
display: flex;
align-items: center;
border: 1px solid var(--line);
border-radius: 9px;
background: #fbfaf6;
overflow: hidden;
}
.money span {
padding: 0 4px 0 14px;
color: var(--muted);
font-weight: 600;
}
.money input {
flex: 1;
border: 0;
background: transparent;
font-family: var(--sans);
font-size: 0.95rem;
padding: 12px 14px 12px 4px;
color: var(--ink);
}
.money input:focus,
.money:focus-within {
outline: none;
}
.money:focus-within {
border-color: var(--gold);
}
.hint {
font-size: 0.8rem;
color: var(--muted);
margin-top: 4px;
}
.check {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.86rem;
color: var(--ink);
padding-top: 4px;
border-top: 1px dashed var(--line);
cursor: pointer;
}
.check input {
width: 17px;
height: 17px;
accent-color: var(--navy);
}
/* summary */
.summary {
padding: 26px;
background: linear-gradient(180deg, var(--navy) 0%, var(--navy-d) 100%);
color: #e9ecf3;
position: sticky;
top: 24px;
}
.summary__label {
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.7rem;
font-weight: 600;
color: var(--gold);
}
.summary__total {
font-family: var(--serif);
font-size: 2.7rem;
font-weight: 600;
color: var(--white);
margin: 4px 0 10px;
letter-spacing: -0.02em;
transition: color 0.18s ease;
}
.summary__total.flash {
color: var(--gold-soft);
}
.summary__badge {
display: inline-block;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
padding: 5px 11px;
border-radius: 99px;
background: rgba(176, 141, 87, 0.2);
color: var(--gold-soft);
border: 1px solid rgba(176, 141, 87, 0.45);
}
.breakdown {
list-style: none;
padding: 0;
margin: 20px 0 0;
border-top: 1px solid rgba(176, 141, 87, 0.3);
}
.breakdown li {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 11px 0;
font-size: 0.88rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.breakdown li span:first-child {
color: #b9c1d4;
}
.breakdown li span:last-child {
font-weight: 600;
color: var(--white);
white-space: nowrap;
}
.breakdown li.is-credit span:last-child {
color: #8fd0a8;
}
.summary__note {
font-size: 0.82rem;
color: #b9c1d4;
margin: 16px 0 0;
min-height: 2.4em;
}
.cta {
width: 100%;
margin-top: 20px;
padding: 14px;
border: 0;
border-radius: 9px;
font-family: var(--sans);
font-size: 0.92rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--navy-d);
background: var(--gold);
cursor: pointer;
transition: transform 0.12s ease, background 0.18s ease;
}
.cta:hover {
background: #c19a60;
}
.cta:active {
transform: translateY(1px);
}
.cta:focus-visible {
outline: 2px solid var(--gold-soft);
outline-offset: 3px;
}
.disclaimer {
font-size: 0.72rem;
line-height: 1.5;
color: #8c95ab;
margin-top: 16px;
}
@media (max-width: 520px) {
body {
padding: 24px 14px;
}
.estimator__head h1 {
font-size: 1.7rem;
}
.estimator__grid {
grid-template-columns: 1fr;
gap: 16px;
}
.form,
.summary {
padding: 20px;
}
.summary {
position: static;
}
.summary__total {
font-size: 2.3rem;
}
.segmented {
grid-template-columns: 1fr;
}
}(function () {
"use strict";
var form = document.getElementById("fee-form");
if (!form) return;
var $ = function (id) {
return document.getElementById(id);
};
var els = {
service: $("service"),
hours: $("hours"),
rate: $("rate"),
complexity: $("complexity"),
scope: $("scope"),
recovery: $("recovery"),
pct: $("pct"),
filing: $("filing"),
hoursOut: $("hours-out"),
rateOut: $("rate-out"),
complexityOut: $("complexity-out"),
scopeOut: $("scope-out"),
pctOut: $("pct-out"),
total: $("total"),
badge: $("model-badge"),
breakdown: $("breakdown"),
note: $("note"),
cta: $("cta"),
};
var FILING_COST = 640;
var COMPLEXITY = [
{ label: "Standard", mult: 1 },
{ label: "Complex", mult: 1.35 },
{ label: "Highly complex", mult: 1.75 },
];
var SCOPE = [
{ label: "Essentials", mult: 0.8 },
{ label: "Standard package", mult: 1 },
{ label: "Comprehensive", mult: 1.4 },
];
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function getModel() {
var checked = form.querySelector('input[name="model"]:checked');
return checked ? checked.value : "hourly";
}
function showBlock(model) {
var blocks = document.querySelectorAll(".model-block");
for (var i = 0; i < blocks.length; i++) {
blocks[i].hidden = blocks[i].getAttribute("data-block") !== model;
}
}
function row(label, value, isCredit) {
var li = document.createElement("li");
if (isCredit) li.className = "is-credit";
var a = document.createElement("span");
a.textContent = label;
var b = document.createElement("span");
b.textContent = value;
li.appendChild(a);
li.appendChild(b);
return li;
}
function flashTotal() {
els.total.classList.remove("flash");
/* force reflow so the class re-applies */
void els.total.offsetWidth;
els.total.classList.add("flash");
}
function calc() {
var model = getModel();
showBlock(model);
var serviceLabel =
els.service.options[els.service.selectedIndex].text.trim();
var base = parseFloat(
els.service.options[els.service.selectedIndex].getAttribute("data-base")
);
var items = [];
var total = 0;
var note = "";
var badge = "";
if (model === "hourly") {
badge = "Hourly billing";
var hrs = parseInt(els.hours.value, 10);
var rate = parseInt(els.rate.value, 10);
var cx = COMPLEXITY[parseInt(els.complexity.value, 10)];
els.hoursOut.textContent = hrs + " hrs";
els.rateOut.textContent = "$" + rate + "/hr";
els.complexityOut.textContent = cx.label;
var legal = hrs * rate;
var adj = legal * (cx.mult - 1);
total = legal + adj;
items.push(row("Legal services (" + hrs + " hrs × $" + rate + ")", money(legal)));
if (adj > 0) {
items.push(row(cx.label + " adjustment", money(adj)));
}
note =
"Based on " +
hrs +
" hours at $" +
rate +
"/hr for a " +
cx.label.toLowerCase() +
" " +
serviceLabel.toLowerCase() +
" matter.";
} else if (model === "flat") {
badge = "Fixed flat fee";
var sc = SCOPE[parseInt(els.scope.value, 10)];
els.scopeOut.textContent = sc.label;
var flat = (base || 2400) * sc.mult;
total = flat;
items.push(row(serviceLabel + " — " + sc.label, money(flat)));
note =
"A fixed " +
sc.label.toLowerCase() +
" quote for your " +
serviceLabel.toLowerCase() +
" matter, billed once on engagement.";
} else {
badge = "Contingency (no win, no fee)";
var recovery = parseFloat(els.recovery.value) || 0;
var pct = parseInt(els.pct.value, 10);
els.pctOut.textContent = pct + "%";
var fee = recovery * (pct / 100);
total = fee;
items.push(row("Expected recovery", money(recovery)));
items.push(row("Firm fee (" + pct + "% of recovery)", money(fee)));
var net = recovery - fee;
items.push(row("Estimated net to you", money(net), true));
note =
"You pay " +
pct +
"% only if we recover. On a " +
money(recovery) +
" outcome, your estimated fee is " +
money(fee) +
".";
}
if (els.filing.checked) {
total += FILING_COST;
items.push(row("Filing & court costs", money(FILING_COST)));
}
/* render */
els.breakdown.innerHTML = "";
for (var i = 0; i < items.length; i++) {
els.breakdown.appendChild(items[i]);
}
els.total.textContent = money(total);
els.badge.textContent = badge;
els.note.textContent = note;
flashTotal();
}
form.addEventListener("input", calc);
form.addEventListener("change", calc);
els.cta.addEventListener("click", function () {
var orig = els.cta.textContent;
els.cta.textContent = "Request sent — we'll be in touch";
els.cta.disabled = true;
setTimeout(function () {
els.cta.textContent = orig;
els.cta.disabled = false;
}, 2400);
});
calc();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fee Estimator — Harrow & Vance LLP</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="estimator" aria-labelledby="est-title">
<header class="estimator__head">
<p class="estimator__firm">Harrow & Vance LLP</p>
<h1 id="est-title">Fee Estimator</h1>
<p class="estimator__lede">
Get an indicative range for your matter in under a minute. Adjust the
details below and your estimate updates instantly.
</p>
<span class="rule" aria-hidden="true"></span>
</header>
<div class="estimator__grid">
<!-- ============ CONTROLS ============ -->
<form class="panel form" id="fee-form" novalidate>
<fieldset class="field">
<legend>Practice area</legend>
<label class="select">
<span class="sr-only">Select a practice area</span>
<select id="service" name="service">
<option value="estate" data-base="1850">
Estate planning & wills
</option>
<option value="business" data-base="3200">
Business formation
</option>
<option value="contract" data-base="2400" selected>
Contract review & drafting
</option>
<option value="ip" data-base="4100">
Trademark & IP filing
</option>
<option value="employment" data-base="2900">
Employment dispute
</option>
<option value="injury" data-base="0">
Personal injury claim
</option>
</select>
</label>
</fieldset>
<fieldset class="field">
<legend>Billing model</legend>
<div class="segmented" role="radiogroup" aria-label="Billing model">
<label class="seg">
<input type="radio" name="model" value="hourly" checked />
<span>Hourly</span>
</label>
<label class="seg">
<input type="radio" name="model" value="flat" />
<span>Flat fee</span>
</label>
<label class="seg">
<input type="radio" name="model" value="contingency" />
<span>Contingency</span>
</label>
</div>
</fieldset>
<!-- HOURLY inputs -->
<div class="model-block" data-block="hourly">
<div class="field">
<div class="field__row">
<label for="hours">Estimated hours</label>
<output id="hours-out" for="hours">12 hrs</output>
</div>
<input
type="range"
id="hours"
name="hours"
min="2"
max="80"
step="1"
value="12"
/>
</div>
<div class="field">
<div class="field__row">
<label for="rate">Senior hourly rate</label>
<output id="rate-out" for="rate">$425/hr</output>
</div>
<input
type="range"
id="rate"
name="rate"
min="225"
max="750"
step="25"
value="425"
/>
</div>
<div class="field">
<div class="field__row">
<label for="complexity">Matter complexity</label>
<output id="complexity-out" for="complexity">Standard</output>
</div>
<input
type="range"
id="complexity"
name="complexity"
min="0"
max="2"
step="1"
value="0"
/>
</div>
</div>
<!-- FLAT inputs -->
<div class="model-block" data-block="flat" hidden>
<div class="field">
<div class="field__row">
<label for="scope">Scope of engagement</label>
<output id="scope-out" for="scope">Standard package</output>
</div>
<input
type="range"
id="scope"
name="scope"
min="0"
max="2"
step="1"
value="1"
/>
</div>
<p class="hint">
Flat fees are quoted once we confirm scope. The figure shown is the
firm's typical fixed price for this matter.
</p>
</div>
<!-- CONTINGENCY inputs -->
<div class="model-block" data-block="contingency" hidden>
<div class="field">
<label for="recovery">Expected recovery</label>
<div class="money">
<span aria-hidden="true">$</span>
<input
type="number"
id="recovery"
name="recovery"
min="0"
step="1000"
value="120000"
inputmode="numeric"
/>
</div>
</div>
<div class="field">
<div class="field__row">
<label for="pct">Contingency rate</label>
<output id="pct-out" for="pct">33%</output>
</div>
<input
type="range"
id="pct"
name="pct"
min="25"
max="40"
step="1"
value="33"
/>
</div>
<p class="hint">
You owe nothing unless we recover on your behalf. Court costs may
still apply.
</p>
</div>
<label class="check">
<input type="checkbox" id="filing" name="filing" />
<span>Include filing & court costs (est. $640)</span>
</label>
</form>
<!-- ============ SUMMARY ============ -->
<aside class="panel summary" aria-live="polite" aria-label="Estimate">
<p class="summary__label">Estimated total</p>
<p class="summary__total" id="total">$5,100</p>
<span class="summary__badge" id="model-badge">Hourly billing</span>
<ul class="breakdown" id="breakdown">
<!-- populated by JS -->
</ul>
<p class="summary__note" id="note">
Based on 12 hours at $425/hr.
</p>
<button type="button" class="cta" id="cta">
Book a free consultation
</button>
<p class="disclaimer">
This estimate is for informational purposes only. It is not legal
advice and does not constitute a binding quote or an attorney–client
relationship.
</p>
</aside>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Fee Estimator
A trust-first cost estimator styled for a boutique firm. Clients choose a service type and a billing model — hourly, flat fee or contingency — and the form reshapes itself to ask only the relevant questions. Hourly mode exposes estimated hours, a senior hourly rate and a complexity multiplier; flat mode shows a fixed quoted range; contingency mode asks for the expected recovery and applies the firm’s percentage.
The right-hand summary updates live as you drag the sliders, showing a running line-item breakdown (legal services, complexity adjustment, filing and court costs, and any retainer), a prominent estimated total, and a contextual note explaining how that number was reached.
Everything is vanilla JS with serif headings, a navy-and-gold palette and accessible labelled controls, so it drops straight into an existing marketing site.
Illustrative UI only — not legal advice and not a binding quote.