diff --git a/README.md b/README.md
index 301198c..8bcd6e7 100644
--- a/README.md
+++ b/README.md
@@ -19,8 +19,9 @@ broader than the small number of conditions it can safely change automatically.
> Start with the [DeckDoc diagnostic center](docs/wiki/Home.md) if you have a symptom and do not yet
> know which subsystem is responsible.
-Or use [DeckMD](https://deucebucket.github.io/deckdoc/), the private in-browser checklist, to turn a
-symptom into ranked diagnostic branches and the next evidence to collect.
+Or use [DeckMD](https://deucebucket.github.io/deckdoc/), the private in-browser symptom checker. It
+starts with six broad categories, reveals only the connected follow-up questions, removes conflicting
+paths as you answer, and keeps the complete grouped checklist behind **Browse all checks**.
## Problems DeckDoc investigates
diff --git a/docs/assets/app.js b/docs/assets/app.js
index 32ed6b7..ed9e830 100644
--- a/docs/assets/app.js
+++ b/docs/assets/app.js
@@ -3,65 +3,73 @@
const questions = window.DECKDOC_QUESTIONNAIRE || [];
const knowledge = window.DECKDOC_KNOWLEDGE || [];
+ const categories = window.DECKDOC_CATEGORIES || [];
+ const related = window.DECKDOC_RELATED_CHECKS || {};
+ const conflictPairs = window.DECKDOC_CONFLICTS || [];
const selected = new Set();
const optionById = new Map();
const groupByOption = new Map();
- const groupById = new Map(questions.map((q) => [q.id, q]));
+ const groupById = new Map(questions.map((group) => [group.id, group]));
+ const symptomIds = new Set((groupById.get("symptom")?.options || []).map(([id]) => id));
+ const flowOrder = ["timing", "alive", "scope", "environment", "details", "evidence"];
+ let activeCategory = null;
+ let activeSymptom = null;
+ let flowIndex = 0;
questions.forEach((group) => group.options.forEach(([id, label]) => {
optionById.set(id, label);
groupByOption.set(id, group.id);
}));
- const exclusives = [
- ["none-unsafe", "smoke", "swelling", "liquid", "sparking", "port-damage", "hot-off"],
+ const exclusiveSets = [
["lcd", "oled", "model-unknown"],
["steamos", "windows", "other-os"],
["docked", "handheld"],
- ["one-title", "all-titles"]
+ ["one-title", "all-titles"],
+ ["official-dock", "third-party-dock"],
+ ["repro-always", "repro-rare"],
+ [...symptomIds]
];
+ const conflictMap = new Map();
+ conflictPairs.forEach(([left, right, reason]) => {
+ if (!conflictMap.has(left)) conflictMap.set(left, new Map());
+ if (!conflictMap.has(right)) conflictMap.set(right, new Map());
+ conflictMap.get(left).set(right, reason);
+ conflictMap.get(right).set(left, reason);
+ });
+
const suggestionReasons = {
- "sound-works": "Separates a dead/frozen system from one failed output path",
- "input-works": "Shows whether the session is still accepting controls",
- "ssh-works": "Confirms the kernel and network path remain alive",
- "stream-works": "Tests whether rendered frames exist away from the panel",
- "screen-backlight": "Narrows LCD panel power versus scanout; not applicable to OLED",
- "screen-no-light": "Moves an LCD case toward backlight/panel power evidence",
- "external-works": "Separates the internal panel path from the GPU/session",
- "internal-works": "Separates external Alt Mode/dock from the internal display",
- "during-game": "Distinguishes boot/transition failures from load or title paths",
- "after-wake": "Moves device loss toward suspend/resume reinitialization",
- "first-after-days": "Identifies the long-off first-start research pattern",
- "second-boot-works": "Recovery on the second start is a major discriminator",
- "mode-switch": "Points to Gamescope/KWin session transition",
- "dock-transition": "Points to Type-C, Alt Mode, hub, or hotplug state",
- "one-title": "Moves the first branch toward title/Proton/configuration",
- "all-titles": "Moves the first branch toward shared OS/GPU/memory/storage",
- "hard-lock": "Separates a process exit from whole-system recovery failure",
- "returns-library": "Often leaves a title, Steam, or Gamescope boundary",
- "sig-gpu-reset-ok": "Recovery outcome changes the severity and downstream interpretation",
- "sig-gpu-reset-fail": "Repeated failed reset is an escalation boundary",
- "sig-oom": "Identifies a killed process instead of guessing from swap allocation",
- "connected-icon": "Separates firmware/device loss from route/DNS/service stages",
- "device-missing": "Stronger than a disconnected or administratively down device",
- "local-network": "Separates local link/gateway from DNS or Steam service",
- "fan-zero": "Needs temperature context before it means anything",
- "hot-now": "Zero RPM plus ≥70°C is a stop-load condition",
- "io-errors": "Separates a Steam library path problem from block/media risk",
+ "sound-works": "Tells us whether only one output path failed",
+ "input-works": "Checks whether the session still accepts controls",
+ "ssh-works": "Confirms the OS is still alive remotely",
+ "stream-works": "Checks whether frames exist away from the panel",
+ "screen-backlight": "Separates LCD backlight from scanout",
+ "screen-no-light": "Moves an LCD case toward power or panel evidence",
+ "external-works": "Separates the internal panel from the GPU/session",
+ "during-game": "Separates boot/transition trouble from load",
+ "after-wake": "Points toward suspend/resume reinitialization",
+ "first-after-days": "Matches the long-off first-start pattern",
+ "second-boot-works": "A major cold-start discriminator",
+ "one-title": "Moves toward a title, Proton, or configuration path",
+ "all-titles": "Moves toward a shared OS, GPU, memory, or storage path",
+ "hard-lock": "Separates a process crash from a whole-session failure",
+ "connected-icon": "Moves beyond Wi-Fi firmware to route or DNS",
+ "device-missing": "Stronger than a disconnected device",
+ "fan-zero": "Needs live temperature context",
+ "hot-now": "A stopped fan while hot is a stop-load condition",
+ "io-errors": "Separates a library problem from media risk",
"read-only": "A forced read-only transition is a data-risk signal",
- "multi-dock-failure": "Several dock functions failing together reveal the common upstream path",
- "known-good-accessory": "Known-good A/B moves the boundary away from the accessory",
- "firmware-also-fails": "Persistence outside the installed OS raises hardware suspicion",
- "previous-image-fixes": "Same hardware working on the previous image is strong regression evidence",
- "plugins-off-fixes": "A reversible clean run can isolate third-party state",
- "windows": "Prevents SteamOS-only signatures and fixes from being misapplied",
- "lcd": "Backlight and Vangogh-specific paths may apply",
- "oled": "Avoids treating OLED as an LCD with a missing backlight"
+ "multi-dock-failure": "Several failed dock functions reveal a shared path",
+ "known-good-accessory": "Moves the boundary away from the accessory",
+ "firmware-also-fails": "Raises hardware suspicion across environments",
+ "previous-image-fixes": "Strong evidence of a software regression",
+ "plugins-off-fixes": "Can isolate third-party state",
+ "windows": "Prevents SteamOS-only checks from being misapplied",
+ "lcd": "Enables LCD backlight and Vangogh-specific branches",
+ "oled": "Removes LCD-only backlight questions"
};
- const related = window.DECKDOC_RELATED_CHECKS || {};
-
const evidenceBoosts = {
"sig-display-gap": ["live-internal-display-gap", 34],
"sig-gpu-timeout": ["cross-title-gpu-session", 16],
@@ -80,26 +88,26 @@
};
const genericRoutes = {
- display: ["Display symptom needs its timing and survivor checks", "path", "Select what stays alive, when it goes black, LCD/OLED, and external-display behavior.", "wiki/Display-and-Gamescope-Problems"],
- boot: ["Boot/power symptom needs a preboot boundary", "os", "Record LEDs/chime/BIOS reachability, update history, and whether recovery or Rescue boots.", "wiki/Recovery-and-Escalation"],
- crash: ["Crash scope needs one-title versus system-wide evidence", "app", "Select when it crashes, what survives, how many titles reproduce, and current-incident log signatures.", "wiki/Crashes-GPU-and-Memory"],
- audio: ["Audio symptom needs device-versus-route evidence", "os", "Select wake timing, device missing versus wrong route, model, and SOF evidence.", "wiki/Audio-Problems"],
- network: ["Network symptom needs staged localization", "path", "Select device presence, link state, local gateway, wake timing, and firmware evidence.", "wiki/Network-and-Resume-Problems"],
- thermal: ["Thermal symptom needs a live trend and fan context", "hardware", "Select load, temperature, RPM, suspend, and charging context. Stop load if the fan is stopped while hot.", "wiki/Power-Thermal-and-Battery-Problems"],
- "charge-problem": ["Charging symptom needs direct-versus-dock evidence", "path", "Compare a known-good direct PD supply with the dock/cable path and record exported telemetry.", "wiki/Power-Thermal-and-Battery-Problems"],
- storage: ["Storage symptom needs a data-risk gate", "hardware", "Select device missing/read-only/I/O errors and stop writes when data may be at risk.", "wiki/Storage-and-MicroSD-Problems"],
- dock: ["Dock symptom needs topology and A/B evidence", "path", "Select which dock functions fail together and compare direct known-good paths.", "wiki/Dock-USB-C-and-External-Displays"],
- input: ["Input symptom needs test-UI and cross-environment evidence", "app", "Select one title/control, firmware behavior, wake timing, and Bluetooth/touch branch.", "wiki/Controls-Bluetooth-and-Input"],
- performance: ["Performance symptom needs load-correlated evidence", "os", "Select scope, load, clocks, warm/cold scene, memory pressure, storage activity, and temperature.", "wiki/Crashes-GPU-and-Memory"],
- update: ["Update symptom needs slot/build comparison", "os", "Record exact builds and whether previous image, stable channel, recovery, or Rescue changes the result.", "wiki/DeckDoc-Rescue"]
+ display: ["Display symptom needs its timing and survivor checks", "path", "Tell us what stays alive, when the picture fails, the model, and external-display behavior.", "wiki/Display-and-Gamescope-Problems"],
+ boot: ["Boot or power symptom needs a preboot boundary", "os", "Record LEDs, chime, BIOS reachability, update history, and whether recovery boots.", "wiki/Recovery-and-Escalation"],
+ crash: ["Crash scope needs one-title versus system-wide evidence", "app", "Separate one game from the whole session, then align current logs with the failure.", "wiki/Crashes-GPU-and-Memory"],
+ audio: ["Audio symptom needs device-versus-route evidence", "os", "Separate a missing device from a wrong route and check wake timing.", "wiki/Audio-Problems"],
+ network: ["Network symptom needs staged localization", "path", "Separate device presence, link, local network, DNS, and resume behavior.", "wiki/Network-and-Resume-Problems"],
+ thermal: ["Thermal symptom needs a live trend and fan context", "hardware", "Correlate load, temperature, RPM, suspend, and charging context.", "wiki/Power-Thermal-and-Battery-Problems"],
+ "charge-problem": ["Charging symptom needs direct-versus-dock evidence", "path", "Compare a known-good direct supply with the dock and cable path.", "wiki/Power-Thermal-and-Battery-Problems"],
+ storage: ["Storage symptom needs a data-risk gate", "hardware", "Stop writes when needed, then separate the device, filesystem, and library path.", "wiki/Storage-and-MicroSD-Problems"],
+ dock: ["Dock symptom needs topology and A/B evidence", "path", "Identify which dock functions fail together and compare direct paths.", "wiki/Dock-USB-C-and-External-Displays"],
+ input: ["Input symptom needs test-UI and cross-environment evidence", "app", "Separate one layout or control from device loss across environments.", "wiki/Controls-Bluetooth-and-Input"],
+ performance: ["Performance symptom needs load-correlated evidence", "os", "Compare scope, load, clocks, memory, storage, and temperature.", "wiki/Crashes-GPU-and-Memory"],
+ update: ["Update symptom needs slot and build comparison", "os", "Record exact builds and whether previous image or Rescue changes the result.", "wiki/DeckDoc-Rescue"]
};
+ const stage = document.querySelector("#guided-stage");
const stack = document.querySelector("#question-stack");
- const suggestionRail = document.querySelector("#suggestion-rail");
const rankedResults = document.querySelector("#ranked-results");
function escapeHtml(value) {
- return String(value).replace(/[&<>"]/g, (c) => ({"&":"&","<":"<",">":">",'"':"""}[c]));
+ return String(value).replace(/[&<>"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[character]));
}
function guideHref(route) {
@@ -107,32 +115,60 @@
return `https://github.com/deucebucket/deckdoc/${String(route).replace(/^wiki\//, "wiki/")}`;
}
- function renderQuestions() {
- stack.innerHTML = questions.map((group, index) => `
-
-
- ${escapeHtml(group.eyebrow)} ${escapeHtml(group.title)} ${escapeHtml(group.hint)}
- 0 selected
-
-
- ${group.options.map(([id, label]) => `${escapeHtml(label)} `).join("")}
-
- `).join("");
-
- stack.addEventListener("click", (event) => {
- const button = event.target.closest("[data-option]");
- if (!button) return;
- toggleOption(button.dataset.option);
- });
+ function unsafeSelections() {
+ return ["smoke", "swelling", "liquid", "sparking", "port-damage", "hot-off"].filter((id) => selected.has(id));
+ }
+
+ function blockedReason(id) {
+ if (selected.has(id)) return "";
+ for (const [other, reason] of conflictMap.get(id) || []) {
+ if (selected.has(other)) return `${reason} Clear “${optionById.get(other)}” to change this answer.`;
+ }
+ return "";
+ }
+
+ function clearForNewSymptom(nextSymptom) {
+ const safety = [...selected].filter((id) => groupByOption.get(id) === "safety");
+ selected.clear();
+ safety.forEach((id) => selected.add(id));
+ if (nextSymptom) selected.add(nextSymptom);
+ }
+
+ function setCategory(categoryId) {
+ if (activeCategory?.id !== categoryId) clearForNewSymptom(null);
+ activeCategory = categories.find((category) => category.id === categoryId) || null;
+ activeSymptom = null;
+ flowIndex = 0;
+ update();
+ }
+
+ function setSymptom(symptomId) {
+ clearForNewSymptom(symptomId);
+ activeSymptom = symptomId;
+ activeCategory = categories.find((category) => category.symptoms.includes(symptomId)) || activeCategory;
+ flowIndex = 0;
+ update();
}
function toggleOption(id, force = null) {
+ if (!optionById.has(id)) return;
+ if (symptomIds.has(id)) {
+ if (selected.has(id) && force !== true) {
+ clearForNewSymptom(null);
+ activeSymptom = null;
+ } else {
+ setSymptom(id);
+ }
+ return;
+ }
+
const willSelect = force === null ? !selected.has(id) : force;
if (willSelect) {
- const exclusive = exclusives.find((set) => set.includes(id));
+ if (blockedReason(id)) return;
+ if (id === "none-unsafe") ["smoke", "swelling", "liquid", "sparking", "port-damage", "hot-off"].forEach((unsafe) => selected.delete(unsafe));
+ if (["smoke", "swelling", "liquid", "sparking", "port-damage", "hot-off"].includes(id)) selected.delete("none-unsafe");
+ const exclusive = exclusiveSets.find((set) => set.includes(id));
if (exclusive) exclusive.forEach((other) => selected.delete(other));
- if (id === "none-unsafe") ["smoke","swelling","liquid","sparking","port-damage","hot-off"].forEach((x) => selected.delete(x));
- if (["smoke","swelling","liquid","sparking","port-damage","hot-off"].includes(id)) selected.delete("none-unsafe");
selected.add(id);
} else {
selected.delete(id);
@@ -140,33 +176,116 @@
update();
}
- function currentSuggestions() {
- const candidates = [];
- const seen = new Set();
- const primary = questions.find((q) => q.id === "symptom").options.map(([id]) => id).filter((id) => selected.has(id));
- primary.forEach((symptom) => (related[symptom] || []).forEach((id) => {
- if (!selected.has(id) && !seen.has(id)) { seen.add(id); candidates.push(id); }
- }));
- knowledge.forEach((rule) => {
- if (!rule.symptoms.some((id) => selected.has(id))) return;
- [...(rule.requiresAll || []), ...(rule.contextsAny || [])].forEach((id) => {
- if (optionById.has(id) && !selected.has(id) && !seen.has(id)) { seen.add(id); candidates.push(id); }
- });
- });
- return candidates.slice(0, 10);
+ function flowGroups() {
+ if (!activeSymptom) return [];
+ const relevant = new Set(related[activeSymptom] || []);
+ return flowOrder.map((groupId) => {
+ const group = groupById.get(groupId);
+ return { ...group, options: (group?.options || []).filter(([id]) => relevant.has(id)) };
+ }).filter((group) => group.options.length);
+ }
+
+ function optionButton(id, label, context = "guided") {
+ const reason = blockedReason(id);
+ const selectedState = selected.has(id);
+ const helper = context === "guided" ? suggestionReasons[id] : "";
+ return `
+ ${escapeHtml(label)}
+ ${helper ? `${escapeHtml(helper)} ` : ""}
+ ${reason ? `Unavailable with your current answer ` : ""}
+ `;
+ }
+
+ function renderCategoryStage() {
+ stage.innerHTML = `
+
+
01
+
Start broad
Choose the main problem Pick the closest category. You will see only the issues nested under it.
+
+
+ ${categories.map((category) => `
+ ${escapeHtml(category.code)}
+ ${escapeHtml(category.title)}
+ ${escapeHtml(category.description)}
+ →
+ `).join("")}
+
+ Smoke, swelling, liquid, port damage, or electrical heat? Stop here and open the safety checks. `;
+ }
+
+ function renderSymptomStage() {
+ const symptoms = groupById.get("symptom").options.filter(([id]) => activeCategory.symptoms.includes(id));
+ stage.innerHTML = `
+
+
02
+
${escapeHtml(activeCategory.title)}
Which description is closest? Choose one main symptom. Connected symptoms appear as follow-up questions.
+
+ ${symptoms.map(([id, label]) => `${escapeHtml(label)} Continue → `).join("")}
+ Back to categories
`;
+ }
+
+ function renderFollowupStage(groups) {
+ if (flowIndex >= groups.length) {
+ stage.innerHTML = `
+
+
✓
+
Guided check complete
+
Review the likely branches below
+
You can still change any answer, browse every check, or copy the case summary.
+
+
`;
+ return;
+ }
+ const group = groups[flowIndex];
+ const count = group.options.filter(([id]) => selected.has(id)).length;
+ stage.innerHTML = `
+
+
${String(flowIndex + 3).padStart(2, "0")}
+
Connected check
${escapeHtml(group.title)} ${escapeHtml(group.hint)} Choose any that match, or skip if you do not know.
+
+ ${group.options.map(([id, label]) => optionButton(id, label)).join("")}
+
+ Back
+ ${count ? `${count} selected` : "No answer required"}
+ ${flowIndex === groups.length - 1 ? "Finish" : "Continue"}
+
`;
}
- function renderSuggestions(ids) {
- document.querySelectorAll(".option-card.suggested").forEach((el) => el.classList.remove("suggested"));
- ids.forEach((id) => document.querySelector(`[data-option="${CSS.escape(id)}"]`)?.classList.add("suggested"));
- if (!ids.length) {
- suggestionRail.innerHTML = `Select one or more primary symptoms to reveal the most useful follow-up checks. `;
+ function renderGuidedStage() {
+ if (unsafeSelections().length) {
+ stage.innerHTML = `! Safety stop
Do not continue software troubleshooting Disconnect power if safe and contact Steam Support. Clear the physical-danger answer only if it was selected by mistake.
Review safety answers `;
return;
}
- suggestionRail.innerHTML = ids.map((id) => `
-
- ${escapeHtml(optionById.get(id))}${escapeHtml(suggestionReasons[id] || "Adds a useful diagnostic contrast")}
- `).join("");
+ if (!activeCategory) renderCategoryStage();
+ else if (!activeSymptom) renderSymptomStage();
+ else renderFollowupStage(flowGroups());
+ }
+
+ function advancedGroup(category, ids, extraClass = "") {
+ const unique = [...new Set(ids)].filter((id) => optionById.has(id));
+ const count = unique.filter((id) => selected.has(id)).length;
+ return ``;
+ }
+
+ function renderAdvanced() {
+ const assigned = new Set();
+ const safety = groupById.get("safety").options.map(([id]) => id);
+ safety.forEach((id) => assigned.add(id));
+ const blocks = [advancedGroup({ id: "safety", title: "Physical safety", description: "Stop conditions before software troubleshooting" }, safety, "danger-group")];
+ categories.forEach((category) => {
+ const ids = [...category.symptoms];
+ category.symptoms.forEach((symptom) => ids.push(...(related[symptom] || [])));
+ ids.forEach((id) => assigned.add(id));
+ blocks.push(advancedGroup(category, ids));
+ });
+ const remaining = [...optionById.keys()].filter((id) => !assigned.has(id));
+ if (remaining.length) blocks.push(advancedGroup({ id: "context", title: "Additional context & logs", description: "Less common environment and evidence facts" }, remaining));
+ stack.innerHTML = blocks.join("");
}
function ruleScore(rule) {
@@ -186,27 +305,23 @@
}
function getResults() {
- const results = knowledge.map(ruleScore).filter(Boolean).sort((a, b) => b.score - a.score);
- const primary = questions.find((q) => q.id === "symptom").options.map(([id]) => id).filter((id) => selected.has(id));
- const matchedSymptoms = new Set(results.flatMap((r) => r.symptoms));
- primary.forEach((symptom) => {
- if (matchedSymptoms.has(symptom)) return;
- const generic = genericRoutes[symptom];
- if (!generic) return;
- results.push({
- id: `generic-${symptom}`, title: generic[0], layer: generic[1], why: generic[2], link: generic[3],
- kicker: "More discriminating answers needed", confidence: "Unranked branch", score: 12,
- evidence: ["Use the adaptive next-check rail to add timing, survivor, scope, and evidence facts."],
+ const results = knowledge.map(ruleScore).filter(Boolean).sort((left, right) => right.score - left.score);
+ if (activeSymptom && !results.some((result) => result.symptoms.includes(activeSymptom))) {
+ const generic = genericRoutes[activeSymptom];
+ if (generic) results.push({
+ id: `generic-${activeSymptom}`, title: generic[0], layer: generic[1], why: generic[2], link: generic[3],
+ kicker: "More answers will narrow this", confidence: "Starting branch", score: 12,
+ evidence: ["Continue the short guided path or open Browse all checks for an uncommon detail."],
steps: ["Run a full DeckDoc report while the symptom is present when safe."],
- avoid: "Do not apply a fix until its model, time scope, and preconditions match.", matched: [symptom]
+ avoid: "Do not apply a fix until its model, time scope, and preconditions match.", matched: [activeSymptom]
});
- });
- return results.sort((a, b) => b.score - a.score).slice(0, 6);
+ }
+ return results.sort((left, right) => right.score - left.score).slice(0, 3);
}
function layerWeights(results) {
const weights = { app: 2, os: 2, path: 2, hardware: 2 };
- results.forEach((r, index) => { weights[r.layer] += Math.max(8, r.score / (index + 1)); });
+ results.forEach((result, index) => { weights[result.layer] += Math.max(8, result.score / (index + 1)); });
if (selected.has("one-title")) weights.app += 18;
if (selected.has("after-update") || selected.has("previous-image-fixes")) weights.os += 18;
if (selected.has("sound-works") && selected.has("display")) weights.path += 18;
@@ -219,68 +334,77 @@
}
function renderConstellation(weights) {
- const labels = {app:"Application/config", os:"Driver/OS", path:"Device/path", hardware:"Hardware suspicion"};
- Object.entries(weights).forEach(([key, value]) => {
- document.querySelector(`#node-${key}`).style.setProperty("--power", String(value / 100));
- });
- document.querySelector("#layer-legend").innerHTML = Object.entries(weights).map(([key, value]) => `
- ${labels[key]} ${value}
`).join("");
+ const labels = { app: "Application/config", os: "Driver/OS", path: "Device/path", hardware: "Hardware suspicion" };
+ Object.entries(weights).forEach(([key, value]) => document.querySelector(`#node-${key}`).style.setProperty("--power", String(value / 100)));
+ document.querySelector("#layer-legend").innerHTML = Object.entries(weights).map(([key, value]) => `${labels[key]} ${value}
`).join("");
}
function renderResults(results) {
- const primarySelected = questions.find((q) => q.id === "symptom").options.some(([id]) => selected.has(id));
- if (!primarySelected) {
- rankedResults.innerHTML = `Start with what failed. Select a primary symptom. The ranking will become useful after timing and “what still works” answers.
`;
+ if (!activeSymptom) {
+ rankedResults.innerHTML = `Start with one broad problem. DeckMD will ask a short chain of connected questions instead of showing the whole checklist.
`;
return;
}
if (!results.length) {
- rankedResults.innerHTML = `No safe pattern match yet. Add timing, survivor, scope, and evidence checks. Unknown is better than a false diagnosis.
`;
+ rankedResults.innerHTML = `No safe pattern match yet. Unknown is better than a false diagnosis. Finish the guided questions or browse all checks.
`;
return;
}
- rankedResults.innerHTML = results.map((r, index) => `
-
-
-
${String(index + 1).padStart(2, "0")}
-
${escapeHtml(r.kicker)}
${escapeHtml(r.title)}
-
${escapeHtml(r.confidence)}
-
- ${escapeHtml(r.why)}
-
- Open evidence and action boundary · ${r.matched.length} selected facts matched
-
-
Collect ${r.evidence.map((x) => `${escapeHtml(x)} `).join("")}
-
Safe next steps ${r.steps.map((x) => `${escapeHtml(x)} `).join("")}
-
Your matched facts ${r.matched.map((id) => `${escapeHtml(optionById.get(id) || id)} `).join("")}
-
- Avoid: ${escapeHtml(r.avoid)}
- Open the full diagnostic guide →
-
- `).join("");
+ rankedResults.innerHTML = results.map((result, index) => `
+ ${String(index + 1).padStart(2, "0")} ${escapeHtml(result.kicker)}
${escapeHtml(result.title)} ${escapeHtml(result.confidence)}
+ ${escapeHtml(result.why)}
+ Evidence and safe next steps · ${result.matched.length} answers matched
+ Collect ${result.evidence.map((item) => `${escapeHtml(item)} `).join("")} Safe next steps ${result.steps.map((item) => `${escapeHtml(item)} `).join("")} Matched answers ${result.matched.map((id) => `${escapeHtml(optionById.get(id) || id)} `).join("")}
+ Avoid: ${escapeHtml(result.avoid)}
Open the full diagnostic guide →
+ `).join("");
}
function renderSafety() {
- const unsafe = ["smoke","swelling","liquid","sparking","port-damage","hot-off"].filter((id) => selected.has(id));
+ const unsafe = unsafeSelections();
const banner = document.querySelector("#safety-banner");
banner.hidden = !unsafe.length;
- if (unsafe.length) document.querySelector("#safety-copy").textContent = `${unsafe.map((id) => optionById.get(id)).join(", ")}. Disconnect power if safe, do not charge/open/stress-test it, and contact Steam Support.`;
+ if (unsafe.length) document.querySelector("#safety-copy").textContent = `${unsafe.map((id) => optionById.get(id)).join(", ")}. Disconnect power if safe, do not charge, open, or stress-test it, and contact Steam Support.`;
}
function renderFacts() {
const facts = [...selected].filter((id) => optionById.has(id));
document.querySelector("#selection-count").textContent = `${facts.length} selected`;
- document.querySelector("#selected-facts").innerHTML = facts.length ? facts.map((id) => `${escapeHtml(optionById.get(id))} `).join("") : `No incident facts selected yet. `;
+ document.querySelector("#selected-facts").innerHTML = facts.length ? facts.map((id) => `${escapeHtml(optionById.get(id))}× `).join("") : `No incident facts selected yet. `;
}
- function updateProgress() {
- let touched = 0;
- questions.forEach((group) => {
- const count = group.options.filter(([id]) => selected.has(id)).length;
- if (count) touched += 1;
- document.querySelector(`[data-count-for="${group.id}"]`).textContent = `${count} selected`;
+ function renderPath(groups) {
+ const pieces = [`Start `];
+ if (activeCategory) pieces.push(`${escapeHtml(activeCategory.title)} `);
+ if (activeSymptom) pieces.push(`${escapeHtml(optionById.get(activeSymptom))} `);
+ if (activeSymptom && flowIndex < groups.length) pieces.push(`${escapeHtml(groups[flowIndex].title)} `);
+ document.querySelector("#path-trail").innerHTML = pieces.join(`› `);
+ }
+
+ function updateProgress(groups) {
+ let current = 0;
+ let total = 2;
+ if (activeCategory) current = 1;
+ if (activeSymptom) {
+ total += groups.length;
+ current = Math.min(total, 3 + flowIndex);
+ if (flowIndex >= groups.length) current = total;
+ }
+ document.querySelector("#progress-label").textContent = activeSymptom ? `Step ${Math.max(1, current)} of ${total}` : activeCategory ? "Choose a specific symptom" : "Choose a category to begin";
+ document.querySelector("#progress-bar").style.width = `${activeSymptom ? current / total * 100 : activeCategory ? 20 : 0}%`;
+ }
+
+ function syncButtons() {
+ document.querySelectorAll("[data-option]").forEach((button) => {
+ const id = button.dataset.option;
+ const reason = blockedReason(id);
+ button.setAttribute("aria-pressed", selected.has(id) ? "true" : "false");
+ button.disabled = Boolean(reason);
+ button.setAttribute("aria-disabled", reason ? "true" : "false");
+ if (reason) button.title = reason; else button.removeAttribute("title");
+ });
+ document.querySelectorAll(".advanced-group").forEach((group) => {
+ const count = [...group.querySelectorAll("[data-option]")].filter((button) => selected.has(button.dataset.option)).length;
+ const label = group.querySelector("summary em");
+ if (count) label.textContent = `${count} selected`;
});
- document.querySelector("#answered-count").textContent = String(touched);
- document.querySelector("#group-count").textContent = String(questions.length);
- document.querySelector("#progress-bar").style.width = `${touched / questions.length * 100}%`;
}
function updateUrl() {
@@ -291,67 +415,104 @@
}
function update() {
- document.querySelectorAll("[data-option]").forEach((button) => button.setAttribute("aria-pressed", selected.has(button.dataset.option) ? "true" : "false"));
- const suggestions = currentSuggestions();
- renderSuggestions(suggestions);
+ const groups = flowGroups();
+ if (flowIndex > groups.length) flowIndex = groups.length;
+ renderGuidedStage();
+ renderAdvanced();
+ syncButtons();
renderSafety();
- updateProgress();
renderFacts();
+ renderPath(groups);
+ updateProgress(groups);
const results = getResults();
renderResults(results);
renderConstellation(layerWeights(results));
+ document.querySelector(".constellation").hidden = !activeSymptom;
+ document.querySelector(".result-grid").classList.toggle("single-column", !activeSymptom);
+ document.querySelector(".all-checks-count").textContent = `${optionById.size} checks`;
document.querySelector("#os-warning").hidden = !selected.has("windows");
- document.querySelector("#suggestion-explainer").textContent = suggestions.length ? "These unanswered checks create the largest split between the current branches. Click one to add it." : "Choose a primary symptom and DeckMD will offer the answers that split its major branches.";
updateUrl();
}
- function openOption(id) {
- const groupId = groupByOption.get(id);
- const detail = document.querySelector(`[data-group="${CSS.escape(groupId)}"]`);
- if (detail) detail.open = true;
- toggleOption(id, true);
- document.querySelector(`[data-option="${CSS.escape(id)}"]`)?.scrollIntoView({behavior:"smooth", block:"center"});
- }
-
function caseSummary() {
- const lines = ["DeckMD incident checklist", "========================"];
+ const lines = ["DeckMD guided symptom check", "==========================="];
+ if (activeCategory) lines.push(`\nCategory\n- ${activeCategory.title}`);
questions.forEach((group) => {
- const facts = group.options.filter(([id]) => selected.has(id)).map(([,label]) => `- ${label}`);
+ const facts = group.options.filter(([id]) => selected.has(id)).map(([, label]) => `- ${label}`);
if (facts.length) lines.push(`\n${group.title}`, ...facts);
});
const results = getResults();
- if (results.length) lines.push("\nRanked diagnostic branches", ...results.slice(0,5).map((r, i) => `${i+1}. ${r.title} — ${r.confidence}`));
+ if (results.length) lines.push("\nRanked diagnostic branches", ...results.map((result, index) => `${index + 1}. ${result.title} — ${result.confidence}`));
lines.push("\nGenerated locally by DeckMD. Rankings are triage priorities, not diagnoses.");
return lines.join("\n");
}
- async function copyText(text, button) {
+ async function copyText(value, button) {
try {
- await navigator.clipboard.writeText(text);
+ await navigator.clipboard.writeText(value);
const old = button.textContent;
button.textContent = "Copied";
setTimeout(() => { button.textContent = old; }, 1600);
} catch (_) {
const area = document.createElement("textarea");
- area.value = text; document.body.append(area); area.select(); document.execCommand("copy"); area.remove();
+ area.value = value;
+ document.body.append(area);
+ area.select();
+ document.execCommand("copy");
+ area.remove();
}
}
- renderQuestions();
+ renderAdvanced();
const initial = new URLSearchParams(location.search).get("s");
- if (initial) initial.split(",").forEach((id) => { if (optionById.has(id)) selected.add(id); });
+ if (initial) {
+ initial.split(",").forEach((id) => { if (optionById.has(id)) selected.add(id); });
+ activeSymptom = [...selected].find((id) => symptomIds.has(id)) || null;
+ if (activeSymptom) {
+ [...selected].filter((id) => symptomIds.has(id) && id !== activeSymptom).forEach((id) => selected.delete(id));
+ activeCategory = categories.find((category) => category.symptoms.includes(activeSymptom)) || null;
+ }
+ }
- suggestionRail.addEventListener("click", (event) => {
- const chip = event.target.closest("[data-suggestion]");
- if (chip) openOption(chip.dataset.suggestion);
+ stage.addEventListener("click", (event) => {
+ const category = event.target.closest("[data-category]");
+ const symptom = event.target.closest("[data-symptom]");
+ const option = event.target.closest("[data-option]");
+ if (category) setCategory(category.dataset.category);
+ else if (symptom) setSymptom(symptom.dataset.symptom);
+ else if (option) toggleOption(option.dataset.option);
+ else if (event.target.closest("[data-back-category]")) { activeCategory = null; activeSymptom = null; clearForNewSymptom(null); update(); }
+ else if (event.target.closest("[data-flow-back]")) { if (flowIndex > 0) flowIndex -= 1; else { selected.delete(activeSymptom); activeSymptom = null; } update(); }
+ else if (event.target.closest("[data-flow-next]")) { flowIndex += 1; update(); }
+ else if (event.target.closest("[data-open-safety]")) {
+ const all = document.querySelector("#all-checks"); all.open = true;
+ const safety = document.querySelector('[data-advanced-group="safety"]'); safety.open = true;
+ safety.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ });
+
+ stack.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-option]");
+ if (button) toggleOption(button.dataset.option);
+ });
+
+ document.querySelector("#path-trail").addEventListener("click", (event) => {
+ if (event.target.closest("[data-path-start]")) { activeCategory = null; activeSymptom = null; clearForNewSymptom(null); update(); }
+ else if (event.target.closest("[data-path-category]")) { activeSymptom = null; clearForNewSymptom(null); update(); }
+ else if (event.target.closest("[data-path-symptom]")) { flowIndex = 0; update(); }
+ });
+
+ document.querySelector("#selected-facts").addEventListener("click", (event) => {
+ const button = event.target.closest("[data-remove-fact]");
+ if (button) toggleOption(button.dataset.removeFact, false);
});
document.querySelector("#option-search").addEventListener("input", (event) => {
const query = event.target.value.trim().toLowerCase();
- document.querySelectorAll(".question-group").forEach((group) => {
+ document.querySelectorAll(".advanced-group").forEach((group) => {
let matches = 0;
- group.querySelectorAll(".option-card").forEach((button) => {
+ group.querySelectorAll(".advanced-option").forEach((button) => {
const visible = !query || button.dataset.label.includes(query);
button.hidden = !visible;
if (visible) matches += 1;
@@ -362,12 +523,15 @@
});
document.querySelector("#expand-all").addEventListener("click", (event) => {
- const groups = [...document.querySelectorAll(".question-group:not([hidden])")];
+ const groups = [...document.querySelectorAll(".advanced-group:not([hidden])")];
const expand = groups.some((group) => !group.open);
groups.forEach((group) => { group.open = expand; });
- event.currentTarget.textContent = expand ? "Collapse all sections" : "Expand all sections";
+ event.currentTarget.textContent = expand ? "Collapse all groups" : "Expand all groups";
+ });
+
+ document.querySelector("#reset-all").addEventListener("click", () => {
+ selected.clear(); activeCategory = null; activeSymptom = null; flowIndex = 0; update();
});
- document.querySelector("#reset-all").addEventListener("click", () => { selected.clear(); update(); });
document.querySelector("#copy-summary").addEventListener("click", (event) => copyText(caseSummary(), event.currentTarget));
document.querySelector("#copy-link").addEventListener("click", (event) => copyText(location.href, event.currentTarget));
diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg
new file mode 100644
index 0000000..a640b2b
--- /dev/null
+++ b/docs/assets/favicon.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/docs/assets/questionnaire.js b/docs/assets/questionnaire.js
index 1c12008..f497417 100644
--- a/docs/assets/questionnaire.js
+++ b/docs/assets/questionnaire.js
@@ -152,19 +152,98 @@ window.DECKDOC_QUESTIONNAIRE = [
}
];
+// The guided path starts broad, then reveals only the symptom families nested
+// beneath the selected category. Every primary symptom appears exactly once.
+window.DECKDOC_CATEGORIES = [
+ {
+ id: "screen_display",
+ code: "SCREEN",
+ title: "Screen & display",
+ description: "Black, dim, flickering, corrupted, or missing picture",
+ symptoms: ["display"]
+ },
+ {
+ id: "power_boot",
+ code: "POWER",
+ title: "Power, boot & charging",
+ description: "Won't start, boot loops, heat, fan, battery, or charging",
+ symptoms: ["boot", "charge-problem", "thermal"]
+ },
+ {
+ id: "games_performance",
+ code: "GAMES",
+ title: "Games & performance",
+ description: "Crashes, freezes, low FPS, stutter, or low clocks",
+ symptoms: ["crash", "performance"]
+ },
+ {
+ id: "sound_network",
+ code: "LINK",
+ title: "Sound & connectivity",
+ description: "Audio, microphone, Wi-Fi, internet, or resume trouble",
+ symptoms: ["audio", "network"]
+ },
+ {
+ id: "controls_dock",
+ code: "I/O",
+ title: "Controls & accessories",
+ description: "Buttons, touch, Bluetooth, docks, USB, or external gear",
+ symptoms: ["input", "dock"]
+ },
+ {
+ id: "storage_system",
+ code: "SYSTEM",
+ title: "Storage & system updates",
+ description: "NVMe, microSD, corrupt data, updates, rollback, or image health",
+ symptoms: ["storage", "update"]
+ }
+];
+
+// Selecting one side makes the other unavailable until the first answer is
+// cleared. These rules are symmetric and shared by guided and all-check views.
+window.DECKDOC_CONFLICTS = [
+ ["screen-backlight", "screen-no-light", "The LCD backlight cannot be both visibly on and visibly off."],
+ ["oled", "screen-backlight", "OLED models do not use the LCD backlight check."],
+ ["oled", "screen-no-light", "OLED models do not use the LCD backlight check."],
+ ["fan-zero", "fan-reacts", "A stopped fan conflicts with a fan that still reacts to load."],
+ ["device-missing", "interface-down", "A missing device cannot also be present in a DOWN state."],
+ ["device-missing", "connected-icon", "A missing device cannot still report connected."],
+ ["device-missing", "local-network", "A missing network device cannot still reach the local network."],
+ ["interface-down", "connected-icon", "A DOWN interface conflicts with an active connected state."],
+ ["interface-down", "local-network", "A DOWN interface conflicts with working local connectivity."],
+ ["no-response", "power-led", "No response includes no normal LED response."],
+ ["no-response", "sound-works", "No response conflicts with continuing audio."],
+ ["no-response", "input-works", "No response conflicts with controls or haptics still working."],
+ ["no-response", "ssh-works", "No response conflicts with a working SSH session."],
+ ["no-response", "stream-works", "No response conflicts with a live stream or recording."],
+ ["no-response", "fan-reacts", "No response conflicts with a fan reacting to load."],
+ ["whole-system", "sound-works", "A wholly unresponsive system conflicts with continuing audio."],
+ ["whole-system", "input-works", "A wholly unresponsive system conflicts with working controls."],
+ ["whole-system", "ssh-works", "A wholly unresponsive system conflicts with a working SSH session."],
+ ["whole-system", "stream-works", "A wholly unresponsive system conflicts with advancing frames."],
+ ["hard-lock", "input-works", "A hard lock conflicts with controls still being accepted."],
+ ["hard-lock", "ssh-works", "A hard lock conflicts with a working SSH session."],
+ ["hard-lock", "stream-works", "A hard lock conflicts with advancing frames."],
+ ["sig-no-errors", "sig-gpu-timeout", "No matching errors conflicts with a current-incident GPU timeout."],
+ ["sig-no-errors", "sig-sof", "No matching errors conflicts with a current-incident SOF failure."],
+ ["sig-no-errors", "sig-wifi-fw", "No matching errors conflicts with a current-incident firmware failure."],
+ ["sig-no-errors", "sig-ext4", "No matching errors conflicts with a current-incident filesystem error."],
+ ["sig-no-errors", "sig-smart", "No matching errors conflicts with a current SMART warning."]
+];
+
// Ordered follow-ups define the progressive checklist. The first entries are
// the highest-value branch separators for each primary symptom.
window.DECKDOC_RELATED_CHECKS = {
- display: ["sound-works", "screen-backlight", "screen-no-light", "input-works", "ssh-works", "stream-works", "external-works", "during-game", "after-wake", "first-after-days", "second-boot-works", "mode-switch", "dock-transition", "lcd", "oled"],
- boot: ["no-response", "power-led", "sound-works", "logo-freeze", "boot-loop", "first-after-days", "second-boot-works", "after-update", "firmware-also-fails", "rescue-works"],
- crash: ["one-title", "all-titles", "during-game", "game-launch", "returns-library", "hard-lock", "ssh-works", "sig-gpu-timeout", "sig-gpu-reset-ok", "sig-gpu-reset-fail", "sig-oom"],
- audio: ["after-wake", "device-missing", "audio-route", "sig-sof", "lcd", "oled", "bluetooth-device"],
- network: ["after-wake", "device-missing", "interface-down", "connected-icon", "local-network", "sig-wifi-fw", "docked", "vpn"],
- thermal: ["fan-zero", "hot-now", "under-load", "after-wake", "while-charging", "charge-limit", "sig-hot-fan"],
- "charge-problem": ["docked", "third-party-dock", "slow-charge", "battery-jump", "while-charging", "battery-only", "port-damage"],
- storage: ["micro-sd", "replacement-ssd", "device-missing", "read-only", "io-errors", "sig-ext4", "sig-smart", "full-disk"],
- dock: ["third-party-dock", "official-dock", "multi-dock-failure", "external-display-fails", "ethernet-drops", "charge-problem", "dock-transition", "known-good-accessory", "sig-dock"],
- input: ["one-title", "one-control", "firmware-also-fails", "after-wake", "bluetooth-pair", "bluetooth-latency", "touch-phantom"],
- performance: ["one-title", "all-titles", "under-load", "low-clocks", "stutter-warm", "sig-live-swap", "sig-oom"],
- update: ["after-update", "boot-loop", "previous-image-fixes", "stable-fixes", "immutable-root", "rescue-works"]
+ display: ["sound-works", "screen-backlight", "screen-no-light", "input-works", "ssh-works", "stream-works", "external-works", "whole-system", "during-game", "after-wake", "first-after-days", "second-boot-works", "mode-switch", "dock-transition", "lcd", "oled", "sig-display-gap", "sig-panel-incomplete", "sig-gpu-timeout", "sig-no-errors"],
+ boot: ["no-response", "power-led", "sound-works", "logo-freeze", "boot-loop", "cold-boot", "first-after-days", "second-boot-works", "after-update", "firmware-also-fails", "rescue-works", "lcd", "oled", "sig-no-errors", "sig-inaccessible"],
+ crash: ["one-title", "all-titles", "desktop-too", "during-game", "game-launch", "returns-library", "hard-lock", "ssh-works", "after-update", "after-plugin", "plugins-off-fixes", "sig-gpu-timeout", "sig-gpu-reset-ok", "sig-gpu-reset-fail", "sig-page-fault", "sig-gamescope-core", "sig-oom"],
+ audio: ["after-wake", "device-missing", "audio-route", "sig-sof", "lcd", "oled", "bluetooth-device", "restart-fixes", "sig-no-errors"],
+ network: ["after-wake", "device-missing", "interface-down", "connected-icon", "local-network", "sig-wifi-fw", "docked", "vpn", "restart-fixes", "sig-no-errors"],
+ thermal: ["fan-zero", "fan-reacts", "hot-now", "under-load", "idle", "after-wake", "while-charging", "charge-limit", "sig-hot-fan", "repro-always", "repro-rare"],
+ "charge-problem": ["docked", "handheld", "third-party-dock", "official-dock", "slow-charge", "battery-jump", "while-charging", "battery-only", "port-damage", "known-good-accessory", "repro-always", "repro-rare"],
+ storage: ["micro-sd", "replacement-ssd", "device-missing", "read-only", "io-errors", "sig-ext4", "sig-smart", "sig-btrfs", "full-disk", "repro-always", "repro-rare"],
+ dock: ["third-party-dock", "official-dock", "multi-dock-failure", "external-display-fails", "ethernet-drops", "charge-problem", "dock-transition", "display-hotplug", "known-good-accessory", "other-deck", "sig-dock"],
+ input: ["one-title", "one-control", "firmware-also-fails", "after-wake", "bluetooth-device", "bluetooth-pair", "bluetooth-latency", "touch-phantom", "restart-fixes", "repro-always", "repro-rare"],
+ performance: ["one-title", "all-titles", "under-load", "low-clocks", "stutter-warm", "after-update", "after-plugin", "plugins-off-fixes", "sig-live-swap", "sig-oom", "sig-gpu-timeout"],
+ update: ["after-update", "boot-loop", "logo-freeze", "previous-image-fixes", "stable-fixes", "immutable-root", "rescue-works", "full-disk", "sig-inaccessible"]
};
diff --git a/docs/assets/styles.css b/docs/assets/styles.css
index 09a3378..3c50be7 100644
--- a/docs/assets/styles.css
+++ b/docs/assets/styles.css
@@ -104,16 +104,57 @@ nav a:hover, nav a:focus-visible { color: var(--ink); }
.safety-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; color: var(--bg); background: var(--coral); font-weight: 950; }
.safety-banner p { margin: .25rem 0 0; color: #efb8b2; }
-.suggestion-panel { margin: 2rem 0 1rem; padding: 1.1rem; border: 1px solid rgba(103,193,245,.26); border-radius: var(--radius); background: linear-gradient(120deg, rgba(103,193,245,.08), rgba(103,193,245,.01)); }
-.suggestion-heading { display: flex; justify-content: space-between; gap: 2rem; align-items: end; }
-.suggestion-heading h3 { margin: 0; font-size: 1.25rem; }
-.suggestion-heading > p { max-width: 650px; margin: 0; color: var(--muted); font-size: .86rem; }
-.suggestion-rail { display: flex; gap: .6rem; overflow-x: auto; margin-top: 1rem; padding-bottom: .25rem; scrollbar-width: thin; }
-.suggestion-chip { flex: 0 0 auto; max-width: 300px; padding: .68rem .8rem; text-align: left; border: 1px solid #33454e; border-radius: 10px; color: #cfeefe; background: #111b20; cursor: pointer; }
-.suggestion-chip:hover, .suggestion-chip:focus-visible { border-color: var(--blue); }
-.suggestion-chip small { display: block; margin-top: .25rem; color: #7594a3; font-size: .68rem; }
.suggestion-empty { color: var(--dim); font-size: .84rem; }
+.guided-triage { margin-top: 2rem; border: 1px solid #344047; border-radius: 22px; background: linear-gradient(145deg, rgba(19,24,27,.98), rgba(11,14,16,.98)); overflow: hidden; }
+.path-bar { min-height: 70px; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: .9rem 1.15rem; border-bottom: 1px solid var(--line); background: rgba(255,255,255,.015); }
+.path-label { display: block; margin-bottom: .25rem; color: var(--dim); font: 800 .6rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .14em; text-transform: uppercase; }
+.path-trail { display: flex; flex-wrap: wrap; align-items: center; gap: .45rem; }
+.path-trail button, .path-trail span { padding: 0; border: 0; color: var(--muted); background: none; font-size: .76rem; }
+.path-trail button { color: var(--blue); cursor: pointer; }
+.path-trail i { color: #4c5559; font-style: normal; }
+.guided-stage { min-height: 430px; padding: clamp(1.25rem, 4vw, 3rem); }
+.stage-heading { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 1rem; max-width: 900px; margin-bottom: 1.5rem; }
+.stage-heading h3, .stage-complete h3 { margin: 0; font-family: "Arial Narrow", "Roboto Condensed", sans-serif; font-size: clamp(1.8rem, 3.5vw, 3rem); line-height: 1; letter-spacing: -.025em; text-transform: uppercase; }
+.stage-heading p:not(.overline), .stage-complete > p:not(.overline) { max-width: 720px; margin: .55rem 0 0; color: var(--muted); }
+.step-number { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid #41505a; border-radius: 50%; color: var(--blue); font: 850 .72rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; }
+.category-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; }
+.category-card { position: relative; min-height: 180px; padding: 1rem 2.7rem 1rem 1rem; text-align: left; border: 1px solid #30393e; border-radius: 14px; color: var(--ink); background: #0d1113; cursor: pointer; transition: transform .16s ease, border-color .16s ease, background .16s ease; }
+.category-card:hover, .category-card:focus-visible { transform: translateY(-2px); border-color: var(--blue); background: #111a1f; }
+.category-code { display: block; margin-bottom: 2.2rem; color: var(--blue); font: 800 .62rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .14em; }
+.category-card strong { display: block; font-size: 1.1rem; }
+.category-card small { display: block; margin-top: .35rem; color: var(--muted); font-size: .76rem; }
+.category-arrow { position: absolute; right: 1rem; bottom: 1rem; color: var(--blue); font-size: 1.3rem; }
+.safety-shortcut { width: 100%; display: flex; justify-content: space-between; gap: 1rem; margin-top: .8rem; padding: .75rem 1rem; text-align: left; border: 1px solid rgba(255,107,94,.28); border-radius: 10px; color: #dca6a1; background: rgba(255,107,94,.035); cursor: pointer; }
+.safety-shortcut span { color: #9e716d; font-size: .75rem; }
+.symptom-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem; max-width: 1120px; }
+.symptom-card { min-height: 150px; display: flex; flex-direction: column; justify-content: space-between; padding: 1rem; text-align: left; border: 1px solid #334047; border-radius: 14px; color: var(--ink); background: #0e1316; cursor: pointer; }
+.symptom-card:hover, .symptom-card:focus-visible { border-color: var(--blue); background: #121c21; }
+.symptom-card strong { font-size: 1.02rem; line-height: 1.35; }
+.symptom-card span { color: var(--blue); font-size: .74rem; }
+.guided-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; max-width: 1080px; }
+.guided-option { min-height: 88px; }
+.guided-option > span { display: block; }
+.guided-option > small { display: block; margin-top: .3rem; padding-right: 1rem; color: #78919c; font-size: .68rem; }
+.option-card .blocked-copy { color: #8d6562; }
+.option-card:disabled { opacity: .38; cursor: not-allowed; border-style: dashed; }
+.option-card:disabled::after { content: "×"; display: grid; place-items: center; border: 0; color: var(--coral); font-size: .9rem; transform: translateY(-50%); }
+.stage-actions { display: flex; align-items: center; gap: .75rem; margin-top: 1.25rem; }
+.stage-actions.split { max-width: 1080px; justify-content: space-between; }
+.stage-actions > span { color: var(--dim); font-size: .72rem; }
+.stage-complete { max-width: 760px; margin: 2rem auto; text-align: center; }
+.complete-mark { display: grid; place-items: center; width: 58px; height: 58px; margin: 0 auto 1rem; border: 1px solid var(--green); border-radius: 50%; color: var(--green); font-size: 1.4rem; }
+.stage-complete .stage-actions { justify-content: center; }
+.danger-stage .complete-mark { border-color: var(--coral); color: var(--coral); }
+
+.all-checks { margin-top: 1rem; border: 1px solid var(--line); border-radius: 16px; background: #0d1012; }
+.all-checks > summary { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 1rem 1.15rem; cursor: pointer; list-style: none; }
+.all-checks > summary::-webkit-details-marker { display: none; }
+.all-checks > summary span:first-child { display: grid; }
+.all-checks > summary strong { font-size: .95rem; }
+.all-checks > summary small { color: var(--muted); font-size: .72rem; }
+.all-checks-count { color: var(--blue); font: 750 .68rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; }
+.all-checks-body { padding: 0 1rem 1rem; border-top: 1px solid var(--line); }
.checker-tools { display: flex; gap: .75rem; align-items: center; margin: 1rem 0; }
.search-box { flex: 1; display: flex; align-items: center; gap: .5rem; min-height: 42px; padding: 0 .8rem; border: 1px solid var(--line); border-radius: 10px; background: #0c0f11; }
.search-box input { width: 100%; border: 0; outline: 0; color: var(--ink); background: transparent; }
@@ -121,32 +162,32 @@ nav a:hover, nav a:focus-visible { color: var(--ink); }
.danger-text { color: #d98e87; }
.question-stack { display: grid; gap: .75rem; }
-.question-group { border: 1px solid var(--line); border-radius: 16px; background: var(--panel); overflow: clip; }
-.question-group[open] { border-color: #394248; }
-.question-group summary { display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 1.15rem 1.25rem; cursor: pointer; list-style: none; }
-.question-group summary::-webkit-details-marker { display: none; }
-.question-group summary::after { content: "+"; color: var(--blue); font-size: 1.35rem; }
-.question-group[open] summary::after { content: "−"; }
-.question-title h3 { margin: 0; font-size: 1.08rem; }
-.question-title p { margin: .2rem 0 0; color: var(--muted); font-size: .78rem; }
-.group-count { color: var(--dim); font: 700 .68rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; }
-.question-body { padding: 0 1rem 1rem; }
-.option-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .55rem; }
+.advanced-group { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); overflow: clip; }
+.advanced-group[open] { border-color: #394248; }
+.advanced-group > summary { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem; cursor: pointer; list-style: none; }
+.advanced-group > summary::-webkit-details-marker { display: none; }
+.advanced-group > summary span { display: grid; }
+.advanced-group > summary b { font-size: .92rem; }
+.advanced-group > summary small { color: var(--muted); font-size: .7rem; }
+.advanced-group > summary em { color: var(--dim); font: 700 .66rem/1 ui-monospace, SFMono-Regular, Menlo, monospace; font-style: normal; }
+.advanced-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .55rem; padding: 0 .8rem .8rem; }
+.danger-group[open] { border-color: rgba(255,107,94,.45); }
.option-card { position: relative; min-height: 64px; padding: .78rem 2.2rem .78rem .8rem; text-align: left; border: 1px solid #2c3337; border-radius: 10px; color: #bac0c0; background: #0d1012; cursor: pointer; transition: border-color .14s ease, color .14s ease, background .14s ease; }
.option-card::after { content: ""; position: absolute; right: .8rem; top: 50%; width: 14px; height: 14px; border: 1px solid #4b555a; border-radius: 3px; transform: translateY(-50%); }
.option-card:hover, .option-card:focus-visible { border-color: #526169; color: var(--ink); }
.option-card[aria-pressed="true"] { border-color: var(--blue); color: #e7f7ff; background: rgba(103,193,245,.09); }
.option-card[aria-pressed="true"]::after { border-color: var(--blue); background: var(--blue); box-shadow: inset 0 0 0 3px #101a1f; }
-.question-group[data-tone="danger"] .option-card[aria-pressed="true"] { border-color: var(--coral); background: rgba(255,107,94,.08); }
-.question-group[data-tone="danger"] .option-card[aria-pressed="true"]::after { border-color: var(--coral); background: var(--coral); }
-.option-card.suggested { box-shadow: inset 3px 0 var(--amber); }
-.option-card[hidden], .question-group[hidden] { display: none; }
+.danger-group .option-card[aria-pressed="true"] { border-color: var(--coral); background: rgba(255,107,94,.08); }
+.danger-group .option-card[aria-pressed="true"]::after { border-color: var(--coral); background: var(--coral); }
+.option-card[hidden], .advanced-group[hidden] { display: none; }
.results { margin-top: 3rem; padding-top: 2.5rem; border-top: 1px solid var(--line); }
.result-actions { display: flex; gap: .5rem; }
.os-warning { margin: 1rem 0; padding: .85rem 1rem; border-left: 3px solid var(--amber); color: #e4cfaa; background: rgba(244,186,92,.07); }
.result-grid { display: grid; grid-template-columns: 380px minmax(0, 1fr); gap: 1rem; margin-top: 1.5rem; }
+.result-grid.single-column { grid-template-columns: 1fr; }
.constellation { min-height: 560px; padding: 1.2rem; border: 1px solid var(--line); border-radius: var(--radius); background: #0b0e10; }
+.constellation[hidden] { display: none; }
.constellation h3 { margin: 0; }
.orbit { position: relative; width: 270px; height: 270px; margin: 2.2rem auto; border-radius: 50%; }
.orbit-ring { position: absolute; inset: 12%; border: 1px solid #263036; border-radius: 50%; }
@@ -184,7 +225,9 @@ nav a:hover, nav a:focus-visible { color: var(--ink); }
.case-digest summary { display: flex; justify-content: space-between; padding: .8rem 1rem; cursor: pointer; }
.case-digest summary span { color: var(--muted); font-size: .74rem; }
#selected-facts { display: flex; flex-wrap: wrap; gap: .4rem; padding: 0 1rem 1rem; }
-.fact-chip { padding: .35rem .5rem; border: 1px solid #374045; border-radius: 6px; color: #aeb5b4; font-size: .7rem; }
+.fact-chip { display: inline-flex; align-items: center; gap: .4rem; padding: .35rem .5rem; border: 1px solid #374045; border-radius: 6px; color: #aeb5b4; background: #101416; cursor: pointer; font-size: .7rem; }
+.fact-chip:hover, .fact-chip:focus-visible { border-color: var(--coral); }
+.fact-chip span { color: var(--coral); }
.method-strip { width: min(1320px, calc(100% - 3rem)); margin: 0 auto 7rem; display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.method-strip > div { padding: 2rem 1.5rem; border-right: 1px solid var(--line); }
@@ -201,7 +244,7 @@ footer p { margin: .35rem 0; }
@media (max-width: 1000px) {
.hero { grid-template-columns: 1fr; min-height: auto; padding-top: 5rem; }
.hero-instrument { max-width: 620px; }
- .option-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .category-grid, .advanced-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.result-grid { grid-template-columns: 1fr; }
.constellation { min-height: auto; }
.method-strip { grid-template-columns: repeat(2, 1fr); }
@@ -218,12 +261,21 @@ footer p { margin: .35rem 0; }
.trace-line { grid-template-columns: 28px 1fr; }
.trace-line i, .trace-line small { display: none; }
.checker-shell { width: calc(100% - 1rem); padding: 1rem; border-radius: 18px; }
- .checker-topbar, .results-header, .suggestion-heading { align-items: start; flex-direction: column; gap: 1rem; }
+ .checker-topbar, .results-header { align-items: start; flex-direction: column; gap: 1rem; }
.progress-block { width: 100%; text-align: left; }
+ .path-bar { align-items: flex-start; }
+ .guided-stage { min-height: 0; padding: 1rem; }
+ .stage-heading { grid-template-columns: 42px 1fr; }
+ .step-number { width: 38px; height: 38px; }
+ .category-grid, .symptom-grid, .guided-options, .advanced-grid { grid-template-columns: 1fr; }
+ .category-card { min-height: 142px; }
+ .safety-shortcut { flex-direction: column; }
+ .stage-actions.split { display: grid; grid-template-columns: 1fr 1fr; }
+ .stage-actions.split > span { grid-column: 1 / -1; grid-row: 1; }
+ .stage-actions.split .button { width: 100%; }
.checker-tools { flex-wrap: wrap; }
.search-box { flex-basis: 100%; }
- .option-grid { grid-template-columns: 1fr; }
- .question-group summary { gap: 1rem; padding: 1rem; }
+ .advanced-group summary { gap: 1rem; padding: 1rem; }
.result-actions { width: 100%; }
.result-actions .button { flex: 1; }
.result-card-head { grid-template-columns: auto 1fr; }
@@ -237,7 +289,7 @@ footer p { margin: .35rem 0; }
@media print {
body { color: #111; background: #fff; }
- .site-header, .hero, .suggestion-panel, .checker-tools, .question-stack, .result-actions, .method-strip, footer { display: none !important; }
+ .site-header, .hero, .guided-triage, .all-checks, .checker-tools, .question-stack, .result-actions, .method-strip, footer { display: none !important; }
.checker-shell { width: 100%; margin: 0; border: 0; box-shadow: none; background: #fff; }
.results { border: 0; }
.result-grid { grid-template-columns: 1fr; }
diff --git a/docs/index.html b/docs/index.html
index 429ecca..08e3a17 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -7,6 +7,7 @@
DeckMD — Steam Deck symptom checker by DeckDoc
+
@@ -31,11 +32,10 @@
Steam Deck symptom intelligence · runs entirely in your browser
Don’t guess the fix.Narrow the failure.
-
A symptom can cross six layers before it reaches your screen. Check what
- happened, what survived, and when it changed. DeckMD ranks the next diagnostic branch and
- shows the evidence needed to confirm it.
+
Start with one broad problem. DeckMD asks only the connected questions
+ that help separate the likely paths, then shows what evidence would confirm them.
@@ -55,11 +55,11 @@ Don’t guess the fix.Narrow the failure.
-
Interactive triage matrix
-
Build the incident fingerprint
+
Guided symptom check
+
What’s going wrong?
-
0 of 8 sections touched
+
Choose a category to begin
@@ -69,23 +69,32 @@ Build the incident fingerprint
Stop software troubleshooting.
-
-
-
Adaptive follow-up
Next best checks
-
Choose a primary symptom and DeckMD will offer the answers that split its major branches.
+
-
- ⌕
-
-
- Expand all sections
- Reset
-
-
-
+
+
+ Browse all checks Use the complete grouped list when the guided path does not fit.
+ 128 checks
+
+
+
+ ⌕
+
+
+ Expand all groups
+
+
+
+