Salon — Color Formula Tracker
A per-client hair-color formula tracker for a boutique color bar, pairing a client header with a live formula builder where shade rows carry tinted swatches, parts and developer volume, plus and minus controls, and a gilded ratio bar that recomputes total color, mix ratio and estimated weight on every keystroke. Technique and process-time fields sit beside a save action, while a history rail lists past formulas with swatches and result notes you can load straight back into the builder. Editorial serif, soft toasts.
MCP
Code
:root {
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--gold: #b08d57;
--gold-d: #8c6d3f;
--gold-soft: #efe2cf;
--rose: #c9a78f;
--rose-soft: #f3e6dc;
--ink: #1c1814;
--ink-2: #3d362f;
--muted: #8a7d70;
--cream: #f7f1e8;
--bg: #faf6ef;
--white: #ffffff;
--line: rgba(28, 24, 20, 0.1);
--line-2: rgba(28, 24, 20, 0.18);
--ok: #5f8a6b;
--warn: #c08a3e;
--danger: #b3503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(28, 24, 20, 0.05);
--sh-md: 0 10px 30px -16px rgba(28, 24, 20, 0.35);
--sh-lg: 0 30px 70px -40px rgba(28, 24, 20, 0.45);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 80% -10%, var(--rose-soft), transparent 60%),
radial-gradient(900px 500px at -10% 110%, var(--gold-soft), transparent 55%),
var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2 {
font-family: var(--serif);
font-weight: 600;
margin: 0;
}
button, input, select, output {
font: inherit;
color: inherit;
}
.wrap {
max-width: 1060px;
margin: 0 auto;
padding: clamp(20px, 5vw, 56px) clamp(14px, 4vw, 32px);
}
.caps {
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
/* ---- Sheet ---- */
.sheet {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
overflow: hidden;
}
.sheet__head {
padding: clamp(20px, 4vw, 34px) clamp(20px, 4vw, 40px);
background: linear-gradient(180deg, var(--cream), var(--white));
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 22px;
}
.brand__mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--ink);
color: var(--gold-soft);
font-family: var(--serif);
font-weight: 700;
font-size: 14px;
letter-spacing: 0.04em;
}
.eyebrow {
margin: 0;
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--gold-d);
font-weight: 600;
}
.client {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 16px;
}
.client__avatar {
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(145deg, var(--rose), var(--gold));
color: var(--white);
font-family: var(--serif);
font-weight: 700;
font-size: 20px;
box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.4);
}
.client__name {
font-size: clamp(26px, 5vw, 34px);
line-height: 1.05;
letter-spacing: 0.01em;
}
.client__sub {
margin: 4px 0 0;
color: var(--ink-2);
font-size: 13.5px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.client__sub .dot {
color: var(--line-2);
}
.tag {
display: inline-block;
padding: 2px 9px;
border-radius: 100px;
background: var(--gold-soft);
color: var(--gold-d);
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.02em;
}
.client__stylist {
text-align: right;
display: grid;
gap: 2px;
padding-left: 16px;
border-left: 1px solid var(--line);
}
.client__stylist strong {
font-family: var(--serif);
font-size: 18px;
font-weight: 600;
}
/* ---- Grid ---- */
.grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 0;
}
.panel {
padding: clamp(20px, 4vw, 32px) clamp(20px, 4vw, 36px);
}
.panel--builder {
border-right: 1px solid var(--line);
}
.panel--history {
background: linear-gradient(180deg, var(--white), var(--cream));
}
.panel__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.panel__title {
font-size: 22px;
}
.badge {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
padding: 4px 10px;
border-radius: 100px;
background: var(--ink);
color: var(--gold-soft);
}
.badge--ghost {
background: transparent;
color: var(--gold-d);
border: 1px solid var(--line-2);
}
/* ---- Rows ---- */
.rowhead,
.row {
display: grid;
grid-template-columns: 1fr 84px 96px 30px;
align-items: center;
gap: 10px;
}
.rowhead {
padding: 0 4px 8px;
}
.rowhead span {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.rows {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.row {
padding: 10px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
box-shadow: var(--sh-sm);
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
animation: rowin 0.28s ease;
}
@keyframes rowin {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.row:hover {
border-color: var(--line-2);
box-shadow: var(--sh-md);
}
.row__shade {
display: flex;
align-items: center;
gap: 9px;
min-width: 0;
}
.swatch {
flex: none;
width: 22px;
height: 22px;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), var(--sh-sm);
}
.row select,
.row input {
width: 100%;
border: 1px solid transparent;
background: var(--cream);
border-radius: var(--r-sm);
padding: 8px 9px;
font-size: 13.5px;
transition: border-color 0.16s ease, background 0.16s ease;
min-width: 0;
}
.row__shade select {
font-weight: 600;
}
.row input {
text-align: center;
}
.row select:focus,
.row input:focus {
outline: none;
border-color: var(--gold);
background: var(--white);
}
.row__rm {
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--white);
color: var(--muted);
cursor: pointer;
font-size: 16px;
line-height: 1;
display: grid;
place-items: center;
transition: all 0.16s ease;
}
.row__rm:hover {
border-color: var(--danger);
color: var(--danger);
background: #fdf3f1;
}
.addrow {
margin-top: 12px;
width: 100%;
padding: 11px;
border: 1px dashed var(--line-2);
background: transparent;
border-radius: var(--r-md);
color: var(--gold-d);
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.16s ease;
}
.addrow:hover {
border-color: var(--gold);
background: var(--gold-soft);
}
.addrow__plus {
font-weight: 700;
}
/* ---- Readout ---- */
.readout {
margin-top: 22px;
padding: 16px 18px;
border-radius: var(--r-md);
background: var(--ink);
color: var(--cream);
}
.readout__bar {
height: 12px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.12);
overflow: hidden;
display: flex;
margin-bottom: 14px;
}
.readout__bar span {
height: 100%;
transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.readout__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.stat {
display: grid;
gap: 3px;
}
.stat .caps {
color: rgba(239, 226, 207, 0.6);
}
.stat strong {
font-family: var(--serif);
font-size: 21px;
font-weight: 600;
letter-spacing: 0.01em;
}
/* ---- Process ---- */
.process {
margin-top: 22px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.field {
display: grid;
gap: 7px;
}
.field--wide {
grid-column: 1 / -1;
}
.field select,
.field input {
border: 1px solid var(--line);
background: var(--white);
border-radius: var(--r-sm);
padding: 10px 12px;
font-size: 14px;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.field select:focus,
.field input:focus {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px var(--gold-soft);
}
.stepper {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--white);
padding: 5px 8px;
}
.stepper output {
font-family: var(--serif);
font-size: 20px;
font-weight: 600;
min-width: 34px;
text-align: center;
}
.stepper__unit {
font-size: 12px;
color: var(--muted);
margin-right: auto;
}
.stepper__btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--cream);
cursor: pointer;
font-size: 16px;
line-height: 1;
color: var(--ink-2);
transition: all 0.14s ease;
}
.stepper__btn:hover {
border-color: var(--gold);
color: var(--gold-d);
}
/* ---- Save ---- */
.save {
margin-top: 22px;
width: 100%;
padding: 14px;
border: none;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--ink), var(--ink-2));
color: var(--gold-soft);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
cursor: pointer;
box-shadow: var(--sh-md);
transition: transform 0.14s ease, box-shadow 0.14s ease;
}
.save:hover {
transform: translateY(-1px);
box-shadow: var(--sh-lg);
}
.save:active {
transform: translateY(0);
}
/* ---- History ---- */
.history {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 12px;
}
.hist {
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
box-shadow: var(--sh-sm);
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.hist:hover {
border-color: var(--line-2);
box-shadow: var(--sh-md);
}
.hist__top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
}
.hist__date {
font-family: var(--serif);
font-size: 16px;
font-weight: 600;
}
.hist__tech {
font-size: 11px;
color: var(--gold-d);
font-weight: 600;
letter-spacing: 0.04em;
}
.hist__swatches {
display: flex;
gap: 5px;
margin: 10px 0;
}
.hist__swatches .swatch {
width: 18px;
height: 18px;
border-radius: 5px;
}
.hist__note {
margin: 0 0 12px;
font-size: 13px;
color: var(--ink-2);
line-height: 1.45;
}
.hist__load {
border: 1px solid var(--line-2);
background: transparent;
color: var(--ink);
border-radius: 100px;
padding: 6px 16px;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
transition: all 0.16s ease;
}
.hist__load:hover {
border-color: var(--gold);
background: var(--gold-soft);
color: var(--gold-d);
}
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--cream);
padding: 12px 22px;
border-radius: 100px;
font-size: 13.5px;
font-weight: 500;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.26s ease, transform 0.26s ease;
z-index: 50;
border: 1px solid rgba(239, 226, 207, 0.18);
}
.toast--show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast__mark {
color: var(--gold);
font-weight: 700;
margin-right: 6px;
}
/* ---- Responsive ---- */
@media (max-width: 820px) {
.grid {
grid-template-columns: 1fr;
}
.panel--builder {
border-right: none;
border-bottom: 1px solid var(--line);
}
}
@media (max-width: 520px) {
.client {
grid-template-columns: auto 1fr;
row-gap: 14px;
}
.client__stylist {
grid-column: 1 / -1;
text-align: left;
padding-left: 0;
border-left: none;
border-top: 1px solid var(--line);
padding-top: 12px;
display: flex;
align-items: baseline;
gap: 8px;
}
.rowhead {
display: none;
}
.row {
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.row__shade {
grid-column: 1 / -1;
}
.row__rm {
justify-self: end;
}
.process {
grid-template-columns: 1fr;
}
.readout__stats {
grid-template-columns: 1fr;
gap: 8px;
text-align: left;
}
.stat {
grid-template-columns: auto 1fr;
align-items: baseline;
gap: 8px;
}
}(function () {
"use strict";
// ---- Shade catalogue (fictional, salon-realistic) ----
var SHADES = [
{ code: "7N", name: "Natural Blonde", hex: "#a9824f" },
{ code: "8.3", name: "Golden Light Blonde", hex: "#c69a55" },
{ code: "9.13", name: "Beige Very Light Blonde", hex: "#d8bd92" },
{ code: "6.1", name: "Ash Dark Blonde", hex: "#8a7355" },
{ code: "10.21", name: "Pearl Platinum", hex: "#e3d6c0" },
{ code: "5.4", name: "Copper Light Brown", hex: "#7a4f33" },
{ code: "Clear", name: "Clear Gloss", hex: "#efe7d6" },
{ code: "0.11", name: "Intense Ash Booster", hex: "#6f6a63" }
];
var DEVELOPERS = ["10 vol", "20 vol", "30 vol", "40 vol"];
var GRAMS_PER_PART = 15; // tube assumption for est. weight
// ---- Seed builder rows ----
var rowsData = [
{ shade: "8.3", parts: 1, dev: "20 vol" },
{ shade: "9.13", parts: 2, dev: "20 vol" },
{ shade: "0.11", parts: 0.5, dev: "20 vol" }
];
// ---- History ----
var history = [
{
date: "12 May 2026",
tech: "Balayage",
time: 35,
note: "Soft beige blonde at mid-lengths, kept root shadow. Loved the tone.",
rows: [
{ shade: "8.3", parts: 1, dev: "20 vol" },
{ shade: "9.13", parts: 2, dev: "20 vol" },
{ shade: "0.11", parts: 0.5, dev: "20 vol" }
]
},
{
date: "18 Mar 2026",
tech: "Global gloss",
time: 20,
note: "Pearl glaze to kill brass between sessions. Cool but not flat.",
rows: [
{ shade: "Clear", parts: 2, dev: "10 vol" },
{ shade: "10.21", parts: 1, dev: "10 vol" }
]
},
{
date: "02 Feb 2026",
tech: "Root retouch",
time: 40,
note: "Regrowth blend, warm natural base. Slightly warmer than target.",
rows: [
{ shade: "7N", parts: 1, dev: "20 vol" },
{ shade: "6.1", parts: 1, dev: "20 vol" }
]
}
];
// ---- Elements ----
var $ = function (id) { return document.getElementById(id); };
var rowsEl = $("rows");
var historyEl = $("history");
var toastEl = $("toast");
var processTimeEl = $("processTime");
function shadeByCode(code) {
for (var i = 0; i < SHADES.length; i++) {
if (SHADES[i].code === code) return SHADES[i];
}
return SHADES[0];
}
function fmtParts(n) {
return (Math.round(n * 100) / 100).toString();
}
// ---- Render builder rows ----
function renderRows() {
rowsEl.innerHTML = "";
rowsData.forEach(function (r, i) {
var li = document.createElement("li");
li.className = "row";
// Shade cell
var shadeCell = document.createElement("div");
shadeCell.className = "row__shade";
var sw = document.createElement("span");
sw.className = "swatch";
sw.style.background = shadeByCode(r.shade).hex;
var sel = document.createElement("select");
sel.setAttribute("aria-label", "Shade for row " + (i + 1));
SHADES.forEach(function (s) {
var opt = document.createElement("option");
opt.value = s.code;
opt.textContent = s.code + " · " + s.name;
if (s.code === r.shade) opt.selected = true;
sel.appendChild(opt);
});
sel.addEventListener("change", function () {
r.shade = sel.value;
sw.style.background = shadeByCode(r.shade).hex;
update();
});
shadeCell.appendChild(sw);
shadeCell.appendChild(sel);
// Parts cell
var partsInput = document.createElement("input");
partsInput.type = "number";
partsInput.min = "0";
partsInput.step = "0.5";
partsInput.value = r.parts;
partsInput.setAttribute("aria-label", "Parts for row " + (i + 1));
partsInput.addEventListener("input", function () {
var v = parseFloat(partsInput.value);
r.parts = isNaN(v) || v < 0 ? 0 : v;
update();
});
// Developer cell
var devSel = document.createElement("select");
devSel.setAttribute("aria-label", "Developer for row " + (i + 1));
DEVELOPERS.forEach(function (d) {
var opt = document.createElement("option");
opt.value = d;
opt.textContent = d;
if (d === r.dev) opt.selected = true;
devSel.appendChild(opt);
});
devSel.addEventListener("change", function () {
r.dev = devSel.value;
});
// Remove cell
var rm = document.createElement("button");
rm.type = "button";
rm.className = "row__rm";
rm.textContent = "×";
rm.setAttribute("aria-label", "Remove row " + (i + 1));
rm.addEventListener("click", function () {
if (rowsData.length <= 1) {
toast("At least one shade is required");
return;
}
rowsData.splice(i, 1);
renderRows();
update();
});
li.appendChild(shadeCell);
li.appendChild(partsInput);
li.appendChild(devSel);
li.appendChild(rm);
rowsEl.appendChild(li);
});
}
// ---- Update live readout ----
function update() {
var total = rowsData.reduce(function (sum, r) { return sum + (r.parts || 0); }, 0);
$("rowCount").textContent = rowsData.length + (rowsData.length === 1 ? " shade" : " shades");
$("totalParts").textContent = fmtParts(total) + (total === 1 ? " part" : " parts");
$("totalGrams").textContent = Math.round(total * GRAMS_PER_PART) + " g";
// Ratio readout (parts joined by colon, simplified to whole-ish numbers)
var ratioStr;
if (total === 0) {
ratioStr = "—";
} else {
ratioStr = rowsData.map(function (r) { return fmtParts(r.parts || 0); }).join(" : ");
}
$("ratio").textContent = ratioStr;
// Ratio bar
var bar = $("ratioBar");
bar.innerHTML = "";
rowsData.forEach(function (r) {
var seg = document.createElement("span");
var pct = total > 0 ? ((r.parts || 0) / total) * 100 : 0;
seg.style.width = pct + "%";
seg.style.background = shadeByCode(r.shade).hex;
bar.appendChild(seg);
});
}
// ---- Render history ----
function renderHistory() {
historyEl.innerHTML = "";
history.forEach(function (h, i) {
var li = document.createElement("li");
li.className = "hist";
var top = document.createElement("div");
top.className = "hist__top";
var date = document.createElement("span");
date.className = "hist__date";
date.textContent = h.date;
var tech = document.createElement("span");
tech.className = "hist__tech";
tech.textContent = h.tech + " · " + h.time + " min";
top.appendChild(date);
top.appendChild(tech);
var sws = document.createElement("div");
sws.className = "hist__swatches";
h.rows.forEach(function (r) {
var s = document.createElement("span");
s.className = "swatch";
s.style.background = shadeByCode(r.shade).hex;
s.title = r.shade;
sws.appendChild(s);
});
var note = document.createElement("p");
note.className = "hist__note";
note.textContent = h.note;
var load = document.createElement("button");
load.type = "button";
load.className = "hist__load";
load.textContent = "Load into builder";
load.addEventListener("click", function () {
loadFormula(i);
});
li.appendChild(top);
li.appendChild(sws);
li.appendChild(note);
li.appendChild(load);
historyEl.appendChild(li);
});
$("histCount").textContent = history.length + " saved";
}
// ---- Load a past formula ----
function loadFormula(i) {
var h = history[i];
rowsData = h.rows.map(function (r) {
return { shade: r.shade, parts: r.parts, dev: r.dev };
});
processTimeEl.textContent = h.time;
$("resultNote").value = h.note;
// Set technique select if it matches an option
var techSel = $("technique");
for (var k = 0; k < techSel.options.length; k++) {
if (techSel.options[k].value === h.tech) { techSel.selectedIndex = k; break; }
}
renderRows();
update();
toast("Loaded formula from " + h.date);
}
// ---- Save current formula ----
function saveFormula() {
var total = rowsData.reduce(function (sum, r) { return sum + (r.parts || 0); }, 0);
if (total <= 0) {
toast("Add some parts before saving");
return;
}
var now = new Date();
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var dateStr = ("0" + now.getDate()).slice(-2) + " " + months[now.getMonth()] + " " + now.getFullYear();
var note = $("resultNote").value.trim() || "Saved formula — no note added.";
history.unshift({
date: dateStr,
tech: $("technique").value,
time: parseInt(processTimeEl.textContent, 10),
note: note,
rows: rowsData.map(function (r) { return { shade: r.shade, parts: r.parts, dev: r.dev }; })
});
renderHistory();
toast("Formula saved to Aria's record");
}
// ---- Process time stepper ----
function stepTime(delta) {
var v = parseInt(processTimeEl.textContent, 10) || 0;
v = Math.max(5, Math.min(90, v + delta));
processTimeEl.textContent = v;
}
// ---- Toast ----
var toastTimer;
function toast(msg) {
toastEl.innerHTML = '<span class="toast__mark" aria-hidden="true">✓</span>' + msg;
toastEl.classList.add("toast--show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("toast--show");
}, 2600);
}
// ---- Wire up ----
$("addRow").addEventListener("click", function () {
rowsData.push({ shade: "Clear", parts: 1, dev: "20 vol" });
renderRows();
update();
});
$("timeUp").addEventListener("click", function () { stepTime(5); });
$("timeDown").addEventListener("click", function () { stepTime(-5); });
$("saveBtn").addEventListener("click", saveFormula);
// ---- Init ----
renderRows();
update();
renderHistory();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Color Formula · Maison Lumière Salon</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=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<section class="sheet" aria-labelledby="formula-title">
<!-- Header -->
<header class="sheet__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">ML</span>
<p class="eyebrow">Maison Lumière · Color Bar</p>
</div>
<div class="client">
<div class="client__avatar" aria-hidden="true">AV</div>
<div class="client__meta">
<h1 id="formula-title" class="client__name">Aria Vance</h1>
<p class="client__sub">
<span class="tag">Level 7 · Warm</span>
<span class="dot" aria-hidden="true">·</span>
Last service <time datetime="2026-05-12">12 May 2026</time>
</p>
</div>
<div class="client__stylist">
<span class="caps">Colorist</span>
<strong>Lena Marchetti</strong>
</div>
</div>
</header>
<div class="grid">
<!-- Formula builder -->
<div class="panel panel--builder">
<div class="panel__head">
<h2 class="panel__title">Formula Builder</h2>
<span class="badge" id="rowCount">3 shades</span>
</div>
<div class="rowhead" aria-hidden="true">
<span>Shade</span>
<span>Parts</span>
<span>Developer</span>
<span></span>
</div>
<ul class="rows" id="rows" aria-label="Color formula rows"></ul>
<button class="addrow" id="addRow" type="button">
<span class="addrow__plus" aria-hidden="true">+</span> Add shade
</button>
<!-- Live readout -->
<div class="readout" aria-live="polite">
<div class="readout__bar" id="ratioBar" role="img" aria-label="Shade ratio visualization"></div>
<div class="readout__stats">
<div class="stat">
<span class="caps">Total color</span>
<strong id="totalParts">0 parts</strong>
</div>
<div class="stat">
<span class="caps">Ratio</span>
<strong id="ratio">—</strong>
</div>
<div class="stat">
<span class="caps">Est. weight</span>
<strong id="totalGrams">0 g</strong>
</div>
</div>
</div>
<!-- Process -->
<div class="process">
<label class="field">
<span class="caps">Technique</span>
<select id="technique">
<option>Root retouch</option>
<option>Global gloss</option>
<option selected>Balayage</option>
<option>Foil highlights</option>
<option>Toner / glaze</option>
</select>
</label>
<label class="field">
<span class="caps">Process time</span>
<div class="stepper">
<button type="button" class="stepper__btn" id="timeDown" aria-label="Decrease process time">−</button>
<output id="processTime">35</output>
<span class="stepper__unit">min</span>
<button type="button" class="stepper__btn" id="timeUp" aria-label="Increase process time">+</button>
</div>
</label>
<label class="field field--wide">
<span class="caps">Result note</span>
<input id="resultNote" type="text" placeholder="e.g. Soft beige blonde, no brass" />
</label>
</div>
<button class="save" id="saveBtn" type="button">Save formula to record</button>
</div>
<!-- History -->
<aside class="panel panel--history" aria-labelledby="history-title">
<div class="panel__head">
<h2 class="panel__title" id="history-title">Formula History</h2>
<span class="badge badge--ghost" id="histCount">—</span>
</div>
<ul class="history" id="history" aria-label="Past formulas"></ul>
</aside>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Color Formula Tracker
A colorist’s chairside record for Maison Lumière Salon, built around a single guest — here, Aria Vance, a level seven warm. The header carries her name, tone tag, last service date and assigned colorist, while the formula builder beneath stacks shade rows that each hold a tinted swatch, a shade selector, a parts field and a developer volume. Add or remove rows freely; the gilded ratio bar and the readout above it recompute total color, the mix ratio and an estimated tube weight on every change.
Technique and a plus-minus process-time stepper sit alongside a result note, so the whole prescription — what was mixed, how long it processed and how it turned out — lives in one place. Saving a formula stamps it with today’s date and drops it to the top of the history rail with a soft confirming toast.
That history is the quiet workhorse: each past formula shows its date, technique, processing time, a strip of shade swatches and the colorist’s note, and a single Load control rebuilds the entire builder from that record so a favourite mix is one tap away. Everything runs on dependency-free vanilla JavaScript — live ratio math, animated bars, an accessible toast helper — and stays legible and tappable down to a 360px viewport.