DIY — Difficulty Meter
An iFixit-style repairability meter set with a workshop blueprint aesthetic. The main card features a 1-10 SVG arc dial that animates on load, a color-coded verdict pill, and five criterion rows (opening ease, fasteners, modularity, parts, docs) rendered as staggered segmented bars. Three device presets — easy phone, average laptop, glued tablet — swap all data with smooth transitions, plus inline pill badges and a five-wrench mini rating for embedding in cards.
MCP
Code
:root {
--blueprint: #0f2a4a;
--blueprint-d: #0a1e36;
--blueprint-l: #16385f;
--grid: rgba(140, 180, 220, 0.14);
--grid-major: rgba(140, 180, 220, 0.26);
--orange: #ff6b35;
--orange-d: #e0521f;
--orange-soft: #ffe3d6;
--kraft: #d9c7a7;
--kraft-l: #efe6d4;
--ink: #1c2733;
--ink-2: #3d4a57;
--muted: #6b7885;
--bg: #f4f1ea;
--white: #ffffff;
--line: rgba(28, 39, 51, 0.12);
--line-2: rgba(28, 39, 51, 0.2);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow-sm: 0 1px 3px rgba(28, 39, 51, 0.08), 0 4px 14px rgba(28, 39, 51, 0.06);
--shadow-md: 0 6px 24px rgba(28, 39, 51, 0.12), 0 2px 6px rgba(28, 39, 51, 0.08);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
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;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
}
.page {
max-width: 880px;
margin: 0 auto;
padding: 40px 20px 72px;
}
/* ============ Header ============ */
.page-head {
text-align: center;
margin-bottom: 28px;
}
.head-kicker {
display: inline-block;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.18em;
color: var(--orange-d);
background: var(--orange-soft);
border: 1px solid rgba(224, 82, 31, 0.25);
border-radius: 999px;
padding: 4px 12px;
margin-bottom: 14px;
}
.head-title {
margin: 0 0 8px;
font-size: clamp(28px, 5vw, 40px);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.04em;
line-height: 1.1;
}
.head-sub {
margin: 0 auto;
max-width: 480px;
color: var(--muted);
font-size: 15px;
}
/* ============ Preset bar ============ */
.preset-bar {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 22px;
}
.preset-btn {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 12px 14px;
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
cursor: pointer;
font-family: inherit;
text-align: left;
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.12s ease, background 0.18s ease;
}
.preset-btn:hover {
border-color: var(--orange);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.preset-btn:focus-visible {
outline: 3px solid var(--orange);
outline-offset: 2px;
}
.preset-btn:active {
transform: translateY(0);
}
.preset-btn.is-active {
background: var(--blueprint);
border-color: var(--blueprint);
box-shadow: var(--shadow-md);
}
.preset-btn.is-active .preset-name {
color: var(--white);
}
.preset-btn.is-active .preset-model {
color: var(--orange);
}
.preset-icon {
font-size: 18px;
line-height: 1;
margin-bottom: 4px;
}
.preset-name {
font-weight: 700;
font-size: 14px;
color: var(--ink);
}
.preset-model {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
color: var(--muted);
}
/* ============ Meter card ============ */
.meter-card {
background: var(--blueprint);
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px),
linear-gradient(var(--grid-major) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-major) 1px, transparent 1px);
background-size: 8px 8px, 8px 8px, 80px 80px, 80px 80px;
border-radius: var(--r-lg);
padding: 26px 28px 20px;
box-shadow: var(--shadow-md);
color: var(--kraft-l);
}
.meter-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 18px;
}
.meter-card-title {
margin: 0 0 4px;
font-size: 18px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--white);
}
.meter-device {
margin: 0;
font-size: 11px;
letter-spacing: 0.12em;
color: var(--kraft);
transition: opacity 0.25s ease;
}
.verdict-pill {
flex-shrink: 0;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 6px 14px;
border-radius: 999px;
border: 1px solid transparent;
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.verdict-pill[data-tier="easy"] {
background: rgba(47, 158, 111, 0.18);
color: #7fe0b4;
border-color: rgba(47, 158, 111, 0.5);
}
.verdict-pill[data-tier="moderate"] {
background: rgba(217, 138, 43, 0.16);
color: #ffcf8f;
border-color: rgba(217, 138, 43, 0.5);
}
.verdict-pill[data-tier="hard"] {
background: rgba(212, 80, 62, 0.16);
color: #ffab9e;
border-color: rgba(212, 80, 62, 0.55);
}
.meter-body {
display: grid;
grid-template-columns: 240px 1fr;
gap: 30px;
align-items: center;
}
/* --- Dial --- */
.dial-wrap {
position: relative;
text-align: center;
}
.dial {
width: 100%;
max-width: 240px;
display: block;
margin: 0 auto;
overflow: visible;
}
.dial-track {
stroke: rgba(140, 180, 220, 0.18);
}
.dial-fill {
stroke: var(--orange);
transition: stroke 0.4s ease;
}
.dial-ticks line {
stroke: rgba(140, 180, 220, 0.45);
stroke-width: 1.5;
}
.dial-readout {
position: absolute;
left: 0;
right: 0;
top: 52%;
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
pointer-events: none;
}
.dial-score {
font-size: 46px;
font-weight: 700;
line-height: 1;
color: var(--white);
}
.dial-outof {
font-size: 14px;
color: var(--kraft);
}
.dial-verdict {
margin: 10px 0 0;
font-size: 14px;
font-weight: 600;
color: var(--kraft-l);
min-height: 1.5em;
transition: opacity 0.25s ease;
}
/* --- Criteria rows --- */
.criteria {
display: flex;
flex-direction: column;
gap: 13px;
}
.crit-row {
display: grid;
grid-template-columns: 132px 1fr 44px;
align-items: center;
gap: 12px;
}
.crit-label {
font-size: 13px;
font-weight: 600;
color: var(--kraft-l);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.crit-blocks {
display: flex;
gap: 5px;
}
.crit-block {
height: 12px;
flex: 1;
border-radius: 3px;
background: rgba(140, 180, 220, 0.16);
border: 1px solid rgba(140, 180, 220, 0.2);
transform-origin: left center;
transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease;
}
.crit-block.is-filled {
background: var(--orange);
border-color: var(--orange-d);
animation: block-pop 0.32s ease both;
}
.crit-row[data-tier="ok"] .crit-block.is-filled {
background: var(--ok);
border-color: #257f58;
}
.crit-row[data-tier="warn"] .crit-block.is-filled {
background: var(--warn);
border-color: #b06f1f;
}
.crit-row[data-tier="danger"] .crit-block.is-filled {
background: var(--danger);
border-color: #ac3c2d;
}
@keyframes block-pop {
0% {
transform: scaleX(0.3);
opacity: 0;
}
60% {
transform: scaleX(1.08);
opacity: 1;
}
100% {
transform: scaleX(1);
opacity: 1;
}
}
.crit-score {
font-size: 12px;
font-weight: 600;
text-align: right;
color: var(--kraft);
}
/* --- Footer --- */
.meter-foot {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 22px;
padding-top: 14px;
border-top: 1px dashed rgba(140, 180, 220, 0.35);
}
.foot-note {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.14em;
color: var(--kraft);
transition: opacity 0.25s ease;
}
/* ============ Variants ============ */
.variants {
margin-top: 36px;
}
.variants-title {
display: flex;
flex-direction: column;
gap: 4px;
margin: 0 0 16px;
font-size: 18px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.variants-kicker {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.2em;
color: var(--orange-d);
}
.variant-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.variant-card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px;
box-shadow: var(--shadow-sm);
}
.variant-card-title {
margin: 0 0 2px;
font-size: 15px;
font-weight: 700;
}
.variant-card-sub {
margin: 0 0 14px;
font-size: 13px;
color: var(--muted);
}
/* --- Badges --- */
.badge-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.diff-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid transparent;
}
.badge-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
}
.diff-badge.is-easy {
background: rgba(47, 158, 111, 0.12);
color: #1d7a52;
border-color: rgba(47, 158, 111, 0.35);
}
.diff-badge.is-moderate {
background: rgba(217, 138, 43, 0.12);
color: #a3661a;
border-color: rgba(217, 138, 43, 0.35);
}
.diff-badge.is-hard {
background: rgba(212, 80, 62, 0.1);
color: #b03a29;
border-color: rgba(212, 80, 62, 0.35);
}
.badge-demo {
border: 1px dashed var(--line-2);
border-radius: var(--r-sm);
overflow: hidden;
}
.badge-demo-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
font-size: 13px;
transition: background 0.15s ease;
}
.badge-demo-row:hover {
background: var(--kraft-l);
}
.badge-demo-row + .badge-demo-row {
border-top: 1px solid var(--line);
}
.badge-demo-part {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--muted);
flex-shrink: 0;
}
.badge-demo-name {
flex: 1;
font-weight: 500;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* --- Wrenches --- */
.wrench-list {
display: flex;
flex-direction: column;
}
.wrench-row {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 2px;
transition: background 0.15s ease;
border-radius: var(--r-sm);
}
.wrench-row:hover {
background: var(--kraft-l);
}
.wrench-row + .wrench-row {
border-top: 1px solid var(--line);
}
.wrench-label {
flex: 1;
font-size: 13px;
font-weight: 500;
}
.wrench-set {
display: inline-flex;
gap: 3px;
}
.wrench-set svg {
width: 16px;
height: 16px;
display: block;
}
.wrench-set svg.is-on {
color: var(--orange);
}
.wrench-set svg.is-off {
color: rgba(28, 39, 51, 0.18);
}
.wrench-score {
font-size: 11px;
font-weight: 600;
color: var(--muted);
width: 30px;
text-align: right;
}
/* ============ Toast ============ */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 16px);
background: var(--ink);
color: var(--kraft-l);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
padding: 10px 18px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ============ Reduced motion ============ */
@media (prefers-reduced-motion: reduce) {
.crit-block.is-filled {
animation: none;
}
.preset-btn,
.toast {
transition: none;
}
}
/* ============ Responsive ============ */
@media (max-width: 720px) {
.meter-body {
grid-template-columns: 1fr;
gap: 22px;
}
.variant-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.page {
padding: 28px 14px 56px;
}
.preset-bar {
grid-template-columns: 1fr;
}
.preset-btn {
flex-direction: row;
align-items: center;
gap: 10px;
}
.preset-icon {
margin-bottom: 0;
}
.preset-model {
margin-left: auto;
}
.meter-card {
padding: 20px 18px 16px;
}
.meter-card-head {
flex-direction: column;
align-items: flex-start;
}
.crit-row {
grid-template-columns: 104px 1fr 40px;
gap: 8px;
}
.crit-label {
font-size: 12px;
}
.badge-demo-part {
display: none;
}
}/* DIY — Difficulty Meter
Presets swap the data set; the dial arc + criterion blocks animate on every change. */
(function () {
"use strict";
// ---------- Data ----------
var PRESETS = {
phone: {
device: "PX-7 FAIRBIT · 2025 REV B",
score: 9,
verdict: "Repairable — grab a spudger",
time: "EST. REPAIR TIME: 25 MIN",
tools: "TOOLS: PH00 + SPUDGER",
criteria: [
{ label: "Opening ease", score: 5 },
{ label: "Fasteners", score: 5 },
{ label: "Modularity", score: 4 },
{ label: "Part availability", score: 5 },
{ label: "Docs & guides", score: 4 }
]
},
laptop: {
device: "NB-14 CRAFTBOOK · 2024",
score: 6,
verdict: "Repairable with patience",
time: "EST. REPAIR TIME: 1 H 40 MIN",
tools: "TOOLS: T5 TORX + PRY PICKS",
criteria: [
{ label: "Opening ease", score: 4 },
{ label: "Fasteners", score: 3 },
{ label: "Modularity", score: 3 },
{ label: "Part availability", score: 3 },
{ label: "Docs & guides", score: 2 }
]
},
tablet: {
device: "TB-11 SEALDECK · 2025",
score: 2,
verdict: "Glued shut — heat gun territory",
time: "EST. REPAIR TIME: 3 H+ (RISKY)",
tools: "TOOLS: HEAT GUN + SUCTION CUPS",
criteria: [
{ label: "Opening ease", score: 1 },
{ label: "Fasteners", score: 1 },
{ label: "Modularity", score: 2 },
{ label: "Part availability", score: 2 },
{ label: "Docs & guides", score: 1 }
]
}
};
var VERDICT_PILL = {
easy: "Repairable",
moderate: "Fixable",
hard: "Hostile"
};
// ---------- Elements ----------
var dialFill = document.getElementById("dial-fill");
var dialTicks = document.getElementById("dial-ticks");
var dialScore = document.getElementById("dial-score");
var dialVerdict = document.getElementById("dial-verdict");
var verdictPill = document.getElementById("verdict-pill");
var meterDevice = document.getElementById("meter-device");
var criteriaList = document.getElementById("criteria-list");
var footNote = document.getElementById("foot-note");
var footTools = document.getElementById("foot-tools");
var toastEl = document.getElementById("toast");
var presetBtns = Array.prototype.slice.call(
document.querySelectorAll(".preset-btn")
);
var ARC_LEN = dialFill.getTotalLength(); // semicircle path length
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
dialFill.style.strokeDasharray = ARC_LEN + " " + ARC_LEN;
dialFill.style.strokeDashoffset = ARC_LEN;
// ---------- Helpers ----------
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 2200);
}
function tierFor10(score) {
if (score >= 7) return "easy";
if (score >= 4) return "moderate";
return "hard";
}
function tierFor5(score) {
if (score >= 4) return "ok";
if (score >= 3) return "warn";
return "danger";
}
function colorForTier(tier) {
var styles = getComputedStyle(document.documentElement);
if (tier === "easy") return styles.getPropertyValue("--ok").trim();
if (tier === "moderate") return styles.getPropertyValue("--warn").trim();
return styles.getPropertyValue("--danger").trim();
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// ---------- Dial ticks (11 marks, 0..10) ----------
(function buildTicks() {
var cx = 100, cy = 110, rOuter = 96, rInner = 90;
for (var i = 0; i <= 10; i++) {
var angle = Math.PI - (Math.PI * i) / 10; // 180° → 0°
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", (cx + rInner * Math.cos(angle)).toFixed(2));
line.setAttribute("y1", (cy - rInner * Math.sin(angle)).toFixed(2));
line.setAttribute("x2", (cx + rOuter * Math.cos(angle)).toFixed(2));
line.setAttribute("y2", (cy - rOuter * Math.sin(angle)).toFixed(2));
dialTicks.appendChild(line);
}
})();
// ---------- Dial animation ----------
var dialAnim = null;
function animateDial(targetScore) {
if (dialAnim) cancelAnimationFrame(dialAnim);
var fromOffset = parseFloat(dialFill.style.strokeDashoffset) || ARC_LEN;
var fromScore = parseInt(dialScore.textContent, 10) || 0;
var toOffset = ARC_LEN * (1 - targetScore / 10);
var duration = reduceMotion ? 0 : 900;
var start = null;
function frame(ts) {
if (start === null) start = ts;
var t = duration === 0 ? 1 : Math.min((ts - start) / duration, 1);
var e = easeOutCubic(t);
dialFill.style.strokeDashoffset =
fromOffset + (toOffset - fromOffset) * e;
dialScore.textContent = Math.round(
fromScore + (targetScore - fromScore) * e
);
if (t < 1) {
dialAnim = requestAnimationFrame(frame);
} else {
dialAnim = null;
}
}
dialAnim = requestAnimationFrame(frame);
}
// ---------- Criteria rows ----------
function renderCriteria(criteria) {
criteriaList.innerHTML = "";
criteria.forEach(function (crit, rowIdx) {
var row = document.createElement("div");
row.className = "crit-row";
row.setAttribute("data-tier", tierFor5(crit.score));
var label = document.createElement("span");
label.className = "crit-label";
label.textContent = crit.label;
var blocks = document.createElement("div");
blocks.className = "crit-blocks";
blocks.setAttribute("role", "img");
blocks.setAttribute(
"aria-label",
crit.label + ": " + crit.score + " out of 5"
);
for (var i = 0; i < 5; i++) {
var block = document.createElement("span");
block.className = "crit-block";
if (i < crit.score) {
if (reduceMotion) {
block.classList.add("is-filled");
} else {
// stagger: rows cascade, blocks within a row cascade faster
(function (b, delay) {
setTimeout(function () {
b.classList.add("is-filled");
}, delay);
})(block, 120 + rowIdx * 90 + i * 55);
}
}
blocks.appendChild(block);
}
var score = document.createElement("span");
score.className = "crit-score mono";
score.textContent = crit.score + "/5";
row.appendChild(label);
row.appendChild(blocks);
row.appendChild(score);
criteriaList.appendChild(row);
});
}
// ---------- Preset switching ----------
function applyPreset(key, opts) {
var data = PRESETS[key];
if (!data) return;
var tier = tierFor10(data.score);
// text swaps with a light fade
[meterDevice, dialVerdict, footNote, footTools].forEach(function (el) {
el.style.opacity = "0";
});
setTimeout(function () {
meterDevice.textContent = data.device;
dialVerdict.textContent = data.score + " — " + data.verdict;
footNote.textContent = data.time;
footTools.textContent = data.tools;
[meterDevice, dialVerdict, footNote, footTools].forEach(function (el) {
el.style.opacity = "1";
});
}, reduceMotion ? 0 : 180);
verdictPill.setAttribute("data-tier", tier);
verdictPill.textContent = VERDICT_PILL[tier];
dialFill.style.stroke = colorForTier(tier);
animateDial(data.score);
renderCriteria(data.criteria);
presetBtns.forEach(function (btn) {
var active = btn.getAttribute("data-preset") === key;
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-selected", active ? "true" : "false");
});
if (!opts || !opts.silent) {
toast("LOADED PROFILE: " + data.device.split(" · ")[0]);
}
}
presetBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("is-active")) return;
applyPreset(btn.getAttribute("data-preset"));
});
});
// ---------- Wrench ratings ----------
var WRENCH_PATH =
"M21.7 6.9a.5.5 0 0 0-.8-.2l-2.9 2.9-2.6-.5-.5-2.6 2.9-2.9a.5.5 0 0 0-.2-.8 5.9 5.9 0 0 0-6.4 1.3 5.9 5.9 0 0 0-1.4 6.2L2.5 17.6a2.4 2.4 0 0 0 0 3.4l.5.5a2.4 2.4 0 0 0 3.4 0l7.3-7.3a5.9 5.9 0 0 0 6.2-1.4 5.9 5.9 0 0 0 1.8-5.9z";
function wrenchSvg(on) {
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("class", on ? "is-on" : "is-off");
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", WRENCH_PATH);
path.setAttribute("fill", "currentColor");
svg.appendChild(path);
return svg;
}
Array.prototype.forEach.call(
document.querySelectorAll(".wrench-set"),
function (set) {
var rating = parseInt(set.getAttribute("data-rating"), 10) || 0;
for (var i = 1; i <= 5; i++) {
set.appendChild(wrenchSvg(i <= rating));
}
}
);
// ---------- Boot ----------
applyPreset("phone", { silent: true });
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DIY — Difficulty Meter</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&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<!-- ============ Header ============ -->
<header class="page-head">
<span class="head-kicker mono">FIELD MANUAL · FIG. 04</span>
<h1 class="head-title">Repair Difficulty Meter</h1>
<p class="head-sub">
Teardown scores rated by the <strong>WrenchWorks Guild</strong> — pick a device
profile below to see how hard the fix really is.
</p>
</header>
<!-- ============ Preset switcher ============ -->
<div class="preset-bar" role="tablist" aria-label="Device presets">
<button class="preset-btn is-active" role="tab" aria-selected="true" data-preset="phone">
<span class="preset-icon" aria-hidden="true">📱</span>
<span class="preset-name">Easy phone</span>
<span class="preset-model mono">PX-7 FAIRBIT</span>
</button>
<button class="preset-btn" role="tab" aria-selected="false" data-preset="laptop">
<span class="preset-icon" aria-hidden="true">💻</span>
<span class="preset-name">Average laptop</span>
<span class="preset-model mono">NB-14 CRAFTBOOK</span>
</button>
<button class="preset-btn" role="tab" aria-selected="false" data-preset="tablet">
<span class="preset-icon" aria-hidden="true">📟</span>
<span class="preset-name">Glued tablet</span>
<span class="preset-model mono">TB-11 SEALDECK</span>
</button>
</div>
<!-- ============ Main meter card ============ -->
<section class="meter-card" aria-labelledby="meter-title">
<div class="meter-card-head">
<div>
<h2 class="meter-card-title" id="meter-title">Repairability score</h2>
<p class="meter-device mono" id="meter-device">PX-7 FAIRBIT · 2025 REV B</p>
</div>
<span class="verdict-pill" id="verdict-pill" data-tier="easy">Repairable</span>
</div>
<div class="meter-body">
<!-- Dial -->
<div class="dial-wrap">
<svg class="dial" viewBox="0 0 200 120" role="img" aria-labelledby="dial-label">
<title id="dial-label">Repairability score dial</title>
<path class="dial-track" d="M 20 110 A 80 80 0 0 1 180 110"
fill="none" stroke-linecap="round" stroke-width="14" />
<path class="dial-fill" id="dial-fill" d="M 20 110 A 80 80 0 0 1 180 110"
fill="none" stroke-linecap="round" stroke-width="14" />
<g class="dial-ticks" id="dial-ticks" aria-hidden="true"></g>
</svg>
<div class="dial-readout">
<span class="dial-score mono" id="dial-score" aria-live="polite">0</span>
<span class="dial-outof mono">/ 10</span>
</div>
<p class="dial-verdict" id="dial-verdict">Loading…</p>
</div>
<!-- Criteria breakdown -->
<div class="criteria" id="criteria-list" aria-label="Criterion breakdown">
<!-- rows injected by script.js -->
</div>
</div>
<footer class="meter-foot">
<span class="foot-note mono" id="foot-note">EST. REPAIR TIME: 25 MIN</span>
<span class="foot-note mono" id="foot-tools">TOOLS: PH00 + SPUDGER</span>
</footer>
</section>
<!-- ============ Inline variants ============ -->
<section class="variants" aria-labelledby="variants-title">
<h2 class="variants-title" id="variants-title">
<span class="mono variants-kicker">APPENDIX A</span>
Inline variants
</h2>
<div class="variant-grid">
<!-- Pill badges -->
<article class="variant-card">
<h3 class="variant-card-title">Difficulty badges</h3>
<p class="variant-card-sub">Drop into part listings or guide headers.</p>
<div class="badge-row">
<span class="diff-badge is-easy"><span class="badge-dot" aria-hidden="true"></span>Easy</span>
<span class="diff-badge is-moderate"><span class="badge-dot" aria-hidden="true"></span>Moderate</span>
<span class="diff-badge is-hard"><span class="badge-dot" aria-hidden="true"></span>Hard</span>
</div>
<div class="badge-demo">
<div class="badge-demo-row">
<span class="badge-demo-part mono">BAT-PX7-01</span>
<span class="badge-demo-name">Battery swap</span>
<span class="diff-badge is-easy"><span class="badge-dot" aria-hidden="true"></span>Easy</span>
</div>
<div class="badge-demo-row">
<span class="badge-demo-part mono">SCR-NB14-03</span>
<span class="badge-demo-name">Screen replacement</span>
<span class="diff-badge is-moderate"><span class="badge-dot" aria-hidden="true"></span>Moderate</span>
</div>
<div class="badge-demo-row">
<span class="badge-demo-part mono">LGB-TB11-07</span>
<span class="badge-demo-name">Logic board reflow</span>
<span class="diff-badge is-hard"><span class="badge-dot" aria-hidden="true"></span>Hard</span>
</div>
</div>
</article>
<!-- Wrench rating -->
<article class="variant-card">
<h3 class="variant-card-title">Wrench rating</h3>
<p class="variant-card-sub">Five-wrench scale for compact cards.</p>
<div class="wrench-list">
<div class="wrench-row">
<span class="wrench-label">Keycap swap</span>
<span class="wrench-set" role="img" aria-label="Difficulty 1 of 5 wrenches" data-rating="1"></span>
<span class="wrench-score mono">1/5</span>
</div>
<div class="wrench-row">
<span class="wrench-label">Fan cleaning</span>
<span class="wrench-set" role="img" aria-label="Difficulty 2 of 5 wrenches" data-rating="2"></span>
<span class="wrench-score mono">2/5</span>
</div>
<div class="wrench-row">
<span class="wrench-label">Hinge rebuild</span>
<span class="wrench-set" role="img" aria-label="Difficulty 4 of 5 wrenches" data-rating="4"></span>
<span class="wrench-score mono">4/5</span>
</div>
<div class="wrench-row">
<span class="wrench-label">Microsoldering</span>
<span class="wrench-set" role="img" aria-label="Difficulty 5 of 5 wrenches" data-rating="5"></span>
<span class="wrench-score mono">5/5</span>
</div>
</div>
</article>
</div>
</section>
</main>
<div class="toast mono" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Difficulty Meter
A repairability scoring UI in the spirit of teardown sites, dressed in the DIY blueprint style. The hero card sits on a navy grid background and pairs a big semicircular SVG dial — its arc drawn with stroke-dasharray and animated via requestAnimationFrame with an ease-out curve — with a counting score readout and a verdict pill that shifts between green (Repairable), amber (Fixable), and red (Hostile) tiers.
Next to the dial, five criterion rows break the score down: each one is a five-block segmented bar that fills with a cascading stagger, tinted by tier, alongside a mono n/5 score. Three preset buttons (Easy phone, Average laptop, Glued tablet) swap the entire dataset — dial, verdict, criteria, estimated time, and required tools — with fade transitions and a toast confirmation. Everything respects prefers-reduced-motion.
Below the main card, an appendix shows two embeddable variants: difficulty pill badges (Easy / Moderate / Hard) demonstrated inside a parts list with mono part numbers, and a compact five-wrench icon rating built from inline SVG, sized for dense card layouts.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.