Files
design-v3/screens/04-printers.html
T

752 lines
160 KiB
HTML
Raw Normal View History

2026-09-19 11:31:50 -04:00
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Extrudex Printers</title>
<meta name="description" content="Printers screen with stored fields, dropped MQTT state and a Future section, Soft UI v3 mockup.">
<script>
(function () {
var root = document.documentElement, key = 'extrudex-v3-theme', t = null;
try { t = localStorage.getItem(key); } catch (e) {}
var q = /[?&]theme=(light|dark)/.exec(location.search); if (q) t = q[1];
if (t !== 'light' && t !== 'dark') {
t = window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
root.setAttribute('data-theme', t);
function set(v) {
root.setAttribute('data-theme', v);
document.querySelectorAll('[data-set-theme]').forEach(function (b) {
b.setAttribute('aria-pressed', String(b.getAttribute('data-set-theme') === v));
});
try { localStorage.setItem(key, v); } catch (e) {}
}
document.addEventListener('DOMContentLoaded', function () { set(root.getAttribute('data-theme')); });
document.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('[data-set-theme]');
if (b) set(b.getAttribute('data-set-theme'));
});
})();
</script>
<style>
/*
* CubeCraftLabs Design System v3 (proposal): Soft UI / Neumorphism
*
* Built on Design System v2.0 (http://pages.ccl/design-v2.0/, verified
* identical to Extrudex web/src/theme.css at dev 321ce0d on 2026-09-18).
*
* Structure
* 1. Font faces JetBrains Mono, self-hosted woff2, no CDN
* 2. v2.0 base scale kept verbatim, so v2 references still resolve
* 3. v3 theme tokens dark (default, as in v2.0) and light (new)
* 4. Neumorphism surfaces, raised/inset shadow pairs, intensity, radius
* 5. State tokens focus, pressed, disabled, error
* 6. Reference components used by the mockups
*
* Accessibility rules this file enforces:
* - Shadows are decoration. Every control also has a border at 3:1 or more
* against its surface (WCAG 1.4.11), so it stays visible with shadows off.
* - Focus, pressed, disabled and error each change something besides shadow
* direction: the outline, the fill plus a check glyph, a dashed border, or
* a red border plus an icon plus a message.
* - The contrast table is in design-system-v3.md.
*
* Theme selection: dark is the default because v2.0 is dark. Light applies
* when the OS prefers it and nothing is forced, or with data-theme="light" on
* <html>. data-theme="dark" forces dark.
*/
/* ── 1. Font faces ──────────────────────────────────────────────────────── */
/* The same files @fontsource/jetbrains-mono 5.3.0 ships (the app's dependency). */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 400; src: url('data:font/woff2;base64,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
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 500; src: url('data:font/woff2;base64,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
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 700; src: url('data:font/woff2;base64,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
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-display: swap; font-weight: 800; src: url('data:font/woff2;base64,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
/* ── 2. v2.0 base scale (unchanged) ─────────────────────────────────────── */
:root {
--ccl-charcoal: #2c2c30;
--ccl-charcoal-2: #36363a;
--ccl-charcoal-3: #1b1b1e;
--ccl-gold: #8f7ae0;
--ccl-gold-dim: #7a63cc;
--ccl-text: #f4f4f6;
--ccl-text-muted: #b2b2ba;
--ccl-border: #3d3d44;
--ccl-border-2: #4e4e57;
--ccl-green: #a9d88b;
--ccl-red: #f08b93;
--ccl-white: #ffffff;
--ccl-ink: #2a2431;
--ccl-grey: var(--ccl-charcoal);
--ccl-grey-2: var(--ccl-charcoal-2);
--ccl-grey-3: var(--ccl-charcoal-3);
--ccl-purple: var(--ccl-gold);
--ccl-purple-dim: var(--ccl-gold-dim);
--ccl-purple-wash: rgba(143, 122, 224, 0.12);
/* v3 additions to the base scale: tints derived from the v2.0 hues */
--ccl-gold-light: #a99af0; /* accent lifted to pass 4.5:1 as text on charcoal */
--ccl-violet-deep: #5b43b8; /* accent darkened to pass 4.5:1 on the light surface */
--ccl-charcoal-inset:#232327; /* between charcoal and charcoal-3: dark wells */
--ccl-amber: #e8c170; /* new: warning (v2.0 had none) */
--ccl-blue: #8fc3f0; /* new: informational / provenance */
--ccl-mist: #e4e2ea; /* light page: charcoal's hue, inverted */
--ccl-mist-2: #f1f0f5; /* light high surface */
--ccl-mist-3: #d9d6e1; /* light inset */
/* Typography, spacing and motion: unchanged from v2.0 */
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
--font-body: var(--font-mono);
--font-display: var(--font-mono);
--fw-regular: 400; --fw-medium: 500; --fw-bold: 700; --fw-display: 800;
--fs-caption: 11px; --fs-label: 12px; --fs-body: 14px; --fs-body-lg: 15px;
--fs-h3: 18px; --fs-h2: 22px; --fs-h1: 28px; --fs-display: 40px;
--lh-tight: 1.15; --lh-snug: 1.35; --lh-body: 1.7;
--tracking-wordmark: 0.02em; --tracking-caps: 0.08em; --tracking-caps-wide: 0.14em;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px;
--space-6: 24px; --space-8: 32px; --space-10: 40px; --space-14: 56px; --space-16: 64px;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--dur-fast: 100ms; --dur-base: 160ms;
--dur-press: 60ms; /* v3: pressing reacts faster than hover */
--scrollbar-width: 6px;
/* ── 4a. Radius scale (v3; replaces v2.0's "0 is the rule") ── */
--radius-none: 0px; /* tables, code, grid lines: square stays available */
--radius-xs: 4px; /* chips, swatches, inline code */
--radius-sm: 8px; /* inputs, small buttons */
--radius-md: 12px; /* buttons, list rows */
--radius-lg: 18px; /* cards, panels */
--radius-xl: 26px; /* dialogs, the app shell */
--radius-pill: 999px; /* toggles, progress tracks, badges */
/* ── 4b. Shadow geometry: distance + blur per intensity level ── */
--nm-d-1: 2px; --nm-b-1: 5px; /* level 1: chips, small controls */
--nm-d-2: 4px; --nm-b-2: 10px; /* level 2: buttons, rows */
--nm-d-3: 8px; --nm-b-3: 18px; /* level 3: cards, panels */
--nm-d-4: 14px; --nm-b-4: 30px; /* level 4: dialogs */
/* Focus ring geometry: identical in both themes */
--focus-width: 2px;
--focus-offset: 3px;
--border-control-width: 1px;
--border-error-width: 2px;
}
/* ── 3. Theme tokens: DARK (default) ───────────────────────────────────── */
:root,
:root[data-theme="dark"] {
color-scheme: dark;
/* Surfaces */
--surface-page: var(--ccl-charcoal); /* #2c2c30 */
--surface-raised: var(--ccl-charcoal); /* v3: same as page; the lift comes from the shadow */
--surface-high: var(--ccl-charcoal-2); /* #36363a dialogs, menus */
--surface-inset: var(--ccl-charcoal-inset); /* #232327 inputs, wells, progress tracks */
--surface-code: var(--ccl-charcoal-3);
/* Text */
--text-primary: var(--ccl-text); /* 12.66:1 on page */
--text-secondary: var(--ccl-text-muted); /* 6.60:1 on page */
--text-accent: var(--ccl-gold-light); /* 5.67:1 on page */
--text-on-accent: var(--ccl-ink); /* on --accent-fill */
--text-on-gold: var(--ccl-ink);
/* Accent */
--accent: var(--ccl-gold); /* brand hue, for large marks only */
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-gold-light); /* primary button fill; ink on it: 6.14:1 */
--accent-fill-press: #9785e6;
--accent-wash: rgba(169, 154, 240, 0.10);
/* Status: the v2.0 hues plus two new ones */
--status-success: var(--ccl-green);
--status-error: var(--ccl-red);
--status-warning: var(--ccl-amber);
--status-info: var(--ccl-blue);
--wash-success: rgba(169, 216, 139, 0.10);
--wash-error: rgba(240, 139, 147, 0.10);
--wash-warning: rgba(232, 193, 112, 0.10);
--wash-info: rgba(143, 195, 240, 0.10);
/* Borders */
--border-hairline: var(--ccl-border); /* decorative card edges */
--border-strong: var(--ccl-border-2);
--border-control: #85858f; /* 1.4.11: 3.81:1 page, 4.29:1 inset, 3.29:1 high */
--focus-ring: var(--ccl-gold-light); /* 5.67:1 on page */
/* ── 4c. Neumorphism shadow pairs, DARK ──
Dark soft UI fails when the "dark" shadow is only a darker charcoal:
nothing on #2c2c30 is visibly darker than #1b1b1e. Four rules make it
work here:
a. The dark half is true black at high alpha, not a darker gray.
b. The light half is white at 4–7% alpha. It reads as sheen, not glow.
c. Raised surfaces add a 1px top rim light (inset, white 6%). The rim
stays visible even where the drop shadow merges into the background.
d. Every control has a 3:1 border anyway, so the shadow never carries
structure on its own. */
--nm-shadow-dark: rgba(0, 0, 0, 0.62);
--nm-shadow-light: rgba(255, 255, 255, 0.055);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.06);
--nm-inset-dark: rgba(0, 0, 0, 0.55);
--nm-inset-light: rgba(255, 255, 255, 0.045);
--nm-raised-gradient: linear-gradient(145deg, #313136, #29292d);
}
/* ── 3. Theme tokens: LIGHT (new in v3) ────────────────────────────────── */
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
color-scheme: light;
--surface-page: var(--ccl-mist);
--surface-raised: var(--ccl-mist);
--surface-high: var(--ccl-mist-2);
--surface-inset: var(--ccl-mist-3);
--surface-code: #d2cfdb;
--text-primary: var(--ccl-ink);
--text-secondary: #55505f;
--text-accent: var(--ccl-violet-deep);
--text-on-accent: var(--ccl-white);
--text-on-gold: var(--ccl-white);
--accent: var(--ccl-gold);
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-violet-deep);
--accent-fill-press: #4d37a3;
--accent-wash: rgba(91, 67, 184, 0.10);
--status-success: #2c6619;
--status-error: #a8202f;
--status-warning: #7c5100;
--status-info: #1d5e94;
--wash-success: rgba(44, 102, 25, 0.10);
--wash-error: rgba(168, 32, 47, 0.10);
--wash-warning: rgba(124, 81, 0, 0.10);
--wash-info: rgba(29, 94, 148, 0.10);
--border-hairline: #cfccd8;
--border-strong: #b9b5c6;
--border-control: #736e86;
--focus-ring: var(--ccl-violet-deep);
--nm-shadow-dark: rgba(88, 80, 120, 0.30);
--nm-shadow-light: rgba(255, 255, 255, 0.95);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.7);
--nm-inset-dark: rgba(88, 80, 120, 0.28);
--nm-inset-light: rgba(255, 255, 255, 0.9);
--nm-raised-gradient: linear-gradient(145deg, #eceaf1, #dcd9e3);
}
}
:root[data-theme="light"] {
color-scheme: light;
--surface-page: var(--ccl-mist);
--surface-raised: var(--ccl-mist);
--surface-high: var(--ccl-mist-2);
--surface-inset: var(--ccl-mist-3);
--surface-code: #d2cfdb;
--text-primary: var(--ccl-ink);
--text-secondary: #55505f;
--text-accent: var(--ccl-violet-deep);
--text-on-accent: var(--ccl-white);
--text-on-gold: var(--ccl-white);
--accent: var(--ccl-gold);
--accent-hover: var(--ccl-gold-dim);
--accent-fill: var(--ccl-violet-deep);
--accent-fill-press: #4d37a3;
--accent-wash: rgba(91, 67, 184, 0.10);
--status-success: #2c6619;
--status-error: #a8202f;
--status-warning: #7c5100;
--status-info: #1d5e94;
--wash-success: rgba(44, 102, 25, 0.10);
--wash-error: rgba(168, 32, 47, 0.10);
--wash-warning: rgba(124, 81, 0, 0.10);
--wash-info: rgba(29, 94, 148, 0.10);
--border-hairline: #cfccd8;
--border-strong: #b9b5c6;
--border-control: #736e86;
--focus-ring: var(--ccl-violet-deep);
--nm-shadow-dark: rgba(88, 80, 120, 0.30);
--nm-shadow-light: rgba(255, 255, 255, 0.95);
--nm-rim: inset 0 1px 0 rgba(255, 255, 255, 0.7);
--nm-inset-dark: rgba(88, 80, 120, 0.28);
--nm-inset-light: rgba(255, 255, 255, 0.9);
--nm-raised-gradient: linear-gradient(145deg, #eceaf1, #dcd9e3);
}
/* ── 4d. Composed shadows: theme-agnostic, built from the pairs above ── */
:root {
--nm-raised-1: calc(var(--nm-d-1) * -1) calc(var(--nm-d-1) * -1) var(--nm-b-1) var(--nm-shadow-light),
var(--nm-d-1) var(--nm-d-1) var(--nm-b-1) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-2: calc(var(--nm-d-2) * -1) calc(var(--nm-d-2) * -1) var(--nm-b-2) var(--nm-shadow-light),
var(--nm-d-2) var(--nm-d-2) var(--nm-b-2) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-3: calc(var(--nm-d-3) * -1) calc(var(--nm-d-3) * -1) var(--nm-b-3) var(--nm-shadow-light),
var(--nm-d-3) var(--nm-d-3) var(--nm-b-3) var(--nm-shadow-dark), var(--nm-rim);
--nm-raised-4: calc(var(--nm-d-4) * -1) calc(var(--nm-d-4) * -1) var(--nm-b-4) var(--nm-shadow-light),
var(--nm-d-4) var(--nm-d-4) var(--nm-b-4) var(--nm-shadow-dark), var(--nm-rim);
--nm-inset-1: inset var(--nm-d-1) var(--nm-d-1) var(--nm-b-1) var(--nm-inset-dark),
inset calc(var(--nm-d-1) * -1) calc(var(--nm-d-1) * -1) var(--nm-b-1) var(--nm-inset-light);
--nm-inset-2: inset var(--nm-d-2) var(--nm-d-2) var(--nm-b-2) var(--nm-inset-dark),
inset calc(var(--nm-d-2) * -1) calc(var(--nm-d-2) * -1) var(--nm-b-2) var(--nm-inset-light);
--nm-flat: none;
/* v2.0 names kept so old rules still resolve */
--shadow-none: none;
--shadow-raised: var(--nm-raised-2);
/* Borders, v2.0 names */
--border-width: 1px;
--border: var(--border-width) solid var(--border-hairline);
--border-strong-rule: var(--border-width) solid var(--border-strong);
--border-accent: 1.5px solid var(--accent);
--scrollbar-thumb: var(--border-strong);
--scrollbar-thumb-hover: var(--accent);
}
/* ── 6. Reference components ────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { background: var(--surface-page); }
body {
margin: 0;
background: var(--surface-page);
color: var(--text-primary);
font-family: var(--font-body);
font-size: var(--fs-body);
line-height: var(--lh-body);
-webkit-font-smoothing: antialiased;
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
/* Surfaces */
.nm-raised { background: var(--nm-raised-gradient); box-shadow: var(--nm-raised-3); border: 1px solid var(--border-hairline); border-radius: var(--radius-lg); }
.nm-raised-sm { background: var(--surface-raised); box-shadow: var(--nm-raised-2); border: 1px solid var(--border-hairline); border-radius: var(--radius-md); }
.nm-inset { background: var(--surface-inset); box-shadow: var(--nm-inset-2); border: 1px solid var(--border-hairline); border-radius: var(--radius-md); }
.nm-dialog { background: var(--surface-high); box-shadow: var(--nm-raised-4); border: 1px solid var(--border-strong); border-radius: var(--radius-xl); }
/* Buttons
* rest raised-2 + 1px control border
* hover border takes the accent color
* pressed inset shadow + accent wash fill + accent border (+ ✓ on toggles)
* focus 2px solid outline, offset 3px, in the focus color
* disabled no shadow, dashed border, secondary text, not-allowed cursor
*/
.btn {
font: var(--fw-medium) var(--fs-body)/1 var(--font-body);
color: var(--text-primary);
background: var(--surface-raised);
border: var(--border-control-width) solid var(--border-control);
border-radius: var(--radius-md);
padding: 10px 16px;
min-height: 40px;
box-shadow: var(--nm-raised-2);
cursor: pointer;
display: inline-flex; align-items: center; gap: var(--space-2);
transition: box-shadow var(--dur-base) var(--ease-out), border-color var(--dur-fast), background var(--dur-fast);
}
.btn:hover { border-color: var(--text-accent); }
.btn:active, .btn[aria-pressed="true"], .btn.is-pressed {
box-shadow: var(--nm-inset-1);
background: var(--accent-wash);
border-color: var(--text-accent);
transition-duration: var(--dur-press);
}
.btn[aria-pressed="true"]::before, .btn.is-pressed::before { content: "✓"; font-weight: var(--fw-bold); color: var(--text-accent); }
.btn:focus-visible, .btn.is-focused, .input:focus-visible, .input.is-focused, .select:focus-visible, .select.is-focused {
outline: var(--focus-width) solid var(--focus-ring);
outline-offset: var(--focus-offset);
}
.btn:disabled, .btn.is-disabled {
box-shadow: none;
border-style: dashed;
color: var(--text-secondary);
background: transparent;
cursor: not-allowed;
}
.btn-primary { background: var(--accent-fill); color: var(--text-on-accent); border-color: var(--accent-fill); font-weight: var(--fw-bold); }
.btn-primary:hover { border-color: var(--text-primary); }
.btn-primary:active, .btn-primary.is-pressed { background: var(--accent-fill-press); color: var(--text-on-accent); border-color: var(--text-primary); }
.btn-primary.is-pressed::before { color: var(--text-on-accent); }
.btn-danger { color: var(--status-error); border-color: var(--status-error); }
.btn-sm { min-height: 32px; padding: 6px 12px; font-size: var(--fs-label); border-radius: var(--radius-sm); box-shadow: var(--nm-raised-1); }
.btn-icon { width: 40px; padding: 0; justify-content: center; }
/* Inputs: always inset, always bordered */
.input, .select {
font: var(--fw-regular) var(--fs-body)/1.4 var(--font-body);
color: var(--text-primary);
background: var(--surface-inset);
border: var(--border-control-width) solid var(--border-control);
border-radius: var(--radius-sm);
box-shadow: var(--nm-inset-1);
padding: 9px 12px;
min-height: 40px;
width: 100%;
}
.input::placeholder { color: var(--text-secondary); }
.input:disabled, .input.is-disabled, .select:disabled {
box-shadow: none; border-style: dashed; color: var(--text-secondary); background: transparent;
}
/* Error: 2px red border + icon in the label + message. Never color alone. */
.field.has-error .input, .field.has-error .select, .input.is-error {
border: var(--border-error-width) solid var(--status-error);
padding: 8px 11px;
}
.field-error {
display: flex; gap: var(--space-2); align-items: flex-start;
color: var(--status-error); font-size: var(--fs-label); line-height: var(--lh-snug); margin-top: var(--space-1);
}
.field-error::before { content: "⚠"; font-weight: var(--fw-bold); }
/* Chips / badges: text + wash + border + glyph */
.chip {
display: inline-flex; align-items: center; gap: 6px;
font-size: var(--fs-caption); font-weight: var(--fw-bold);
letter-spacing: var(--tracking-caps); text-transform: uppercase;
padding: 2px 9px; border-radius: var(--radius-pill);
border: 1px solid currentColor; line-height: 1.6;
white-space: nowrap;
}
.chip-ok { color: var(--status-success); background: var(--wash-success); }
.chip-err { color: var(--status-error); background: var(--wash-error); }
.chip-warn { color: var(--status-warning); background: var(--wash-warning); }
.chip-info { color: var(--status-info); background: var(--wash-info); }
.chip-accent { color: var(--text-accent); background: var(--accent-wash); }
.chip-neutral { color: var(--text-secondary); background: transparent; }
.chip-dashed { border-style: dashed; }
/* Progress track (inset) + fill (raised) */
.track { height: 10px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-hairline); overflow: hidden; }
.track > span { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--text-accent); }
.track.is-low > span { background: var(--status-error); }
/* Swatch: 1–3 colors, a hairline so white/black filament shows on either theme */
.swatch { display: inline-flex; width: 26px; height: 26px; border-radius: 50%; overflow: hidden; border: 1px solid var(--border-control); box-shadow: var(--nm-raised-1); flex: none; }
.swatch > i { flex: 1; }
.swatch-sm { width: 18px; height: 18px; }
/* Text helpers */
.muted { color: var(--text-secondary); }
.accent { color: var(--text-accent); }
.caps { text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); font-size: var(--fs-caption); font-weight: var(--fw-bold); }
.num { font-variant-numeric: tabular-nums; }
/* ── Mockup app shell (shared by all four screens) ── */
.shell { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--space-6); max-width: 1440px; margin: 0 auto; padding: var(--space-6); min-height: 100vh; }
.rail { position: sticky; top: var(--space-6); align-self: start; padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-5); }
.wordmark { font-weight: var(--fw-display); font-size: var(--fs-h3); letter-spacing: var(--tracking-wordmark); display: flex; align-items: center; gap: var(--space-2); }
.wordmark-mark { width: 30px; height: 30px; border-radius: 10px; background: var(--accent-fill); color: var(--text-on-accent); display: grid; place-items: center; font-size: 15px; box-shadow: var(--nm-raised-1); border: 1px solid var(--border-control); }
.nav { display: flex; flex-direction: column; gap: var(--space-2); }
.nav a { display: flex; align-items: center; gap: var(--space-3); padding: 10px 12px; border-radius: var(--radius-md); color: var(--text-primary); text-decoration: none; border: 1px solid transparent; font-weight: var(--fw-medium); }
.nav a:hover { border-color: var(--border-control); }
.nav a:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: var(--focus-offset); }
.nav a[aria-current="page"] { background: var(--surface-inset); box-shadow: var(--nm-inset-1); border-color: var(--text-accent); color: var(--text-accent); font-weight: var(--fw-bold); }
.nav svg { width: 18px; height: 18px; flex: none; }
.nav .nav-new { margin-left: auto; }
.rail-foot { display: flex; flex-direction: column; gap: var(--space-3); border-top: 1px solid var(--border-hairline); padding-top: var(--space-4); }
.seg { display: inline-flex; padding: 4px; gap: 4px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-control); }
.seg button { font: var(--fw-medium) var(--fs-label)/1 var(--font-body); color: var(--text-secondary); background: transparent; border: 1px solid transparent; border-radius: var(--radius-pill); padding: 7px 12px; cursor: pointer; display: inline-flex; gap: 6px; align-items: center; }
.seg button[aria-pressed="true"] { background: var(--surface-raised); color: var(--text-primary); box-shadow: var(--nm-raised-1); border-color: var(--border-control); font-weight: var(--fw-bold); }
.seg button[aria-pressed="true"]::before { content: "✓"; color: var(--text-accent); }
.seg button:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; }
.who { font-size: var(--fs-label); }
main { min-width: 0; display: flex; flex-direction: column; gap: var(--space-6); padding-bottom: var(--space-16); }
.proposal { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); align-items: center; font-size: var(--fs-label); color: var(--text-secondary); padding: var(--space-3) var(--space-4); }
.page-head { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-end; justify-content: space-between; }
.page-head h1 { margin: 0; font-size: var(--fs-h1); line-height: var(--lh-tight); font-weight: var(--fw-display); }
.page-head p { margin: var(--space-1) 0 0; }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
h2.section { font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); margin: 0 0 var(--space-3); font-weight: var(--fw-bold); display: flex; align-items: center; gap: var(--space-2); }
.card { padding: var(--space-5); }
.card h3 { margin: 0; font-size: var(--fs-body-lg); }
.grid { display: grid; gap: var(--space-5); }
.stack { display: flex; flex-direction: column; gap: var(--space-3); }
.row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.spacer { flex: 1; }
a { color: var(--text-accent); }
a:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
small, .small { font-size: var(--fs-label); }
.tiny { font-size: var(--fs-caption); }
code { font-family: var(--font-mono); font-size: 0.92em; background: var(--surface-inset); padding: 1px 5px; border-radius: var(--radius-xs); border: 1px solid var(--border-hairline); }
.kbd-note { font-size: var(--fs-caption); color: var(--text-secondary); }
/* Data tables: square, numbers right-aligned, inside a soft card */
.table-wrap { overflow-x: auto; margin: 0 calc(var(--space-5) * -1); padding: 0 var(--space-5); }
table.tbl { width: 100%; border-collapse: collapse; border-radius: var(--radius-none); font-size: 13px; }
.tbl th { text-align: left; font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-secondary); font-weight: var(--fw-bold); padding: 10px 10px; border-bottom: 1px solid var(--border-strong); white-space: nowrap; }
.tbl td { padding: 11px 10px; border-bottom: 1px solid var(--border-hairline); vertical-align: middle; }
.tbl tr:last-child td { border-bottom: 0; }
.tbl .r { text-align: right; font-variant-numeric: tabular-nums; }
.tbl tbody tr:hover td { background: var(--accent-wash); }
/* Provenance marker used across screens */
.src { display: inline-flex; gap: 4px; align-items: center; font-size: var(--fs-caption); font-weight: var(--fw-bold); letter-spacing: var(--tracking-caps); text-transform: uppercase; }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
/* Stat tiles */
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-5); }
.stat { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-1); }
.stat-value { font-size: 30px; font-weight: var(--fw-display); line-height: var(--lh-tight); font-variant-numeric: tabular-nums; }
.stat-label { font-size: var(--fs-label); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--tracking-caps); font-weight: var(--fw-bold); }
.stat-foot { font-size: var(--fs-label); color: var(--text-secondary); }
.stat.warn .stat-value { color: var(--status-warning); }
/* Modal backdrop for dialogs */
.scrim { position: fixed; inset: 0; background: rgba(10, 10, 14, 0.55); }
:root[data-theme="light"] .scrim { background: rgba(60, 55, 80, 0.35); }
/* Responsive */
@media (max-width: 1100px) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 860px) {
.shell { grid-template-columns: 1fr; padding: var(--space-4); gap: var(--space-4); }
.rail { position: static; flex-direction: row; flex-wrap: wrap; align-items: center; padding: var(--space-3) var(--space-4); gap: var(--space-3); }
.nav { flex-direction: row; flex-wrap: wrap; gap: var(--space-1); }
.nav a { padding: 8px 10px; }
.nav a span.lbl { display: none; }
.nav a[aria-current="page"] span.lbl { display: inline; }
.nav .nav-new { display: none; }
.rail-foot { border: 0; padding: 0; flex-direction: row; margin-left: auto; }
.who { display: none; }
}
@media (max-width: 520px) {
.stats { grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.stat { padding: var(--space-4); }
.stat-value { font-size: 22px; }
.page-head h1 { font-size: var(--fs-h2); }
}
a.btn { text-decoration: none; }
.th-note { display: block; margin-top: 4px; }
.datasrc { padding: var(--space-4) var(--space-5); display: flex; flex-wrap: wrap; gap: var(--space-3) var(--space-4); align-items: center; }
.group-h { display: flex; align-items: center; gap: var(--space-3); margin: 0 0 var(--space-4); font-size: var(--fs-label); text-transform: uppercase; letter-spacing: var(--tracking-caps-wide); color: var(--text-secondary); font-weight: var(--fw-bold); }
.group-h .n { display: inline-grid; place-items: center; min-width: 24px; height: 24px; border-radius: var(--radius-pill); background: var(--surface-inset); box-shadow: var(--nm-inset-1); border: 1px solid var(--border-hairline); color: var(--text-primary); }
.groups { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: var(--space-6); align-items: start; }
.pgrid { display: grid; gap: var(--space-6); }
.pcard { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
.pcard.attn { border: 2px solid var(--status-warning); }
.phead { display: flex; gap: var(--space-3); align-items: flex-start; }
.phead h3 { margin: 0; font-size: var(--fs-h3); line-height: var(--lh-snug); }
.phead .model { font-size: var(--fs-label); color: var(--text-secondary); }
.cam { aspect-ratio: 16 / 9; border-radius: var(--radius-md); background: radial-gradient(ellipse at 50% 90%, #5a4b3a 0 28%, transparent 29%), linear-gradient(180deg, #1c1d22 0%, #2b2c33 60%, #3a3326 100%); border: 1px solid var(--border-control); box-shadow: var(--nm-inset-2); position: relative; overflow: hidden; }
.cam::after { content: ""; position: absolute; left: 30%; right: 30%; bottom: 20%; height: 22%; background: repeating-linear-gradient(0deg, #6b8fd6 0 3px, #5a7cc2 3px 6px); border-radius: 4px 4px 0 0; opacity: .85; }
.cam .stamp { position: absolute; left: 10px; top: 8px; font-size: var(--fs-caption); color: #f4f4f6; background: rgba(0, 0, 0, .55); padding: 2px 8px; border-radius: var(--radius-pill); z-index: 1; }
.cam-off { min-height: 72px; border-radius: var(--radius-md); border: 1px dashed var(--border-control); display: grid; place-items: center; text-align: center; padding: var(--space-4); color: var(--text-secondary); font-size: var(--fs-label); }
.nowjob { padding: var(--space-4); display: grid; grid-template-columns: 1fr auto; gap: 2px var(--space-3); }
.nowjob .jn { font-weight: var(--fw-bold); }
.slots4 { display: grid; gap: var(--space-2); }
.cont { padding: var(--space-3) var(--space-4); }
.cont-h { display: flex; justify-content: space-between; font-size: var(--fs-label); margin-bottom: var(--space-2); }
.slotrow { display: grid; grid-template-columns: 22px auto minmax(0, 1fr) auto; gap: var(--space-2) var(--space-3); align-items: center; font-size: 13px; padding: 4px 0; }
.slotrow .i { color: var(--text-secondary); font-size: var(--fs-caption); }
.slotrow.empty .nm { color: var(--text-secondary); font-style: italic; }
.conn { display: grid; grid-template-columns: auto 1fr; gap: 4px var(--space-4); font-size: var(--fs-label); margin: 0; }
.conn dt { color: var(--text-secondary); }
.conn dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
details.cfg { border-top: 1px solid var(--border-hairline); padding-top: var(--space-3); }
details.cfg summary { cursor: pointer; font-size: var(--fs-label); font-weight: var(--fw-bold); }
details.cfg summary:focus-visible { outline: var(--focus-width) solid var(--focus-ring); outline-offset: 2px; border-radius: 4px; }
.lost { padding: var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--status-warning); background: var(--wash-warning); display: flex; flex-direction: column; gap: var(--space-2); font-size: 13px; }
.lost h4 { margin: 0; color: var(--status-warning); font-size: 14px; }
.lost ul { margin: 0; padding-left: 18px; }
.stale { opacity: 1; border-style: dashed !important; }
.stale-tag { font-size: var(--fs-caption); color: var(--status-warning); font-weight: var(--fw-bold); }
.pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--status-success); box-shadow: 0 0 0 3px var(--wash-success); }
.future { padding: var(--space-6); border: 2px dashed var(--border-control); border-radius: var(--radius-lg); background: transparent; }
.future h2 { margin: 0; font-size: var(--fs-h3); display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.fgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--space-4); margin-top: var(--space-5); }
.fitem { padding: var(--space-4); border: 1px dashed var(--border-control); border-radius: var(--radius-md); display: flex; flex-direction: column; gap: var(--space-2); }
.fitem h3 { margin: 0; font-size: 14px; }
.fitem .why { font-size: var(--fs-caption); color: var(--text-secondary); line-height: var(--lh-snug); }
.ghost { opacity: .9; filter: grayscale(1); pointer-events: none; }
body.dev-only [data-feature] { display: none !important; }
body:not(.dev-only) [data-devonly] { display: none !important; }
</style>
</head>
<body>
<div class="shell">
<nav class="rail nm-raised" aria-label="Main">
<div class="wordmark"><span class="wordmark-mark" aria-hidden="true">X</span>Extrudex</div>
<div class="nav"><a href="01-dashboard.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="2"/><rect x="14" y="3" width="7" height="5" rx="2"/><rect x="14" y="12" width="7" height="9" rx="2"/><rect x="3" y="16" width="7" height="5" rx="2"/></svg><span class="lbl">Dashboard</span><span class="chip chip-accent nav-new">new</span></a><a href="02-filament.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/><path d="M12 3v6M12 15v6"/></svg><span class="lbl">Filament</span></a><a href="#" title="Prints history: not part of this proposal"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg><span class="lbl">Prints</span></a><a href="04-printers.html" aria-current="page"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 17h8M9 7h6v5H9z"/></svg><span class="lbl">Printers</span></a></div>
<div class="rail-foot">
<div class="seg" role="group" aria-label="Color theme">
<button type="button" data-set-theme="light" aria-pressed="false">Light</button>
<button type="button" data-set-theme="dark" aria-pressed="true">Dark</button>
</div>
<span class="who muted">Signed in · operator</span>
</div>
</nav>
<main id="main">
<div class="proposal nm-inset" role="note">
<span class="chip chip-accent">Design System v3 proposal</span>
<span>Mockup with sample data from the real inventory. Nothing here is wired to the API.</span>
<span class="spacer"></span>
<span class="row" style="gap:12px"><span class="src"><span class="chip chip-neutral">dev</span> on dev today</span>
<span class="src"><span class="chip chip-info chip-dashed">feature</span> feature branch only</span>
<span class="src"><span class="chip chip-warn chip-dashed">API gap</span> stored, not served</span></span>
</div>
<header class="page-head">
<div>
<h1>Printers</h1>
<p class="muted">3 printers · status polled every 10 s · MQTT is read-only</p>
</div>
<div class="actions">
<button class="btn" type="button">Manage AMS units</button>
<button class="btn btn-primary" type="button">+ Add printer</button>
</div>
</header>
<section class="datasrc nm-raised" aria-label="Data source">
<span class="small"><b>Data shown</b></span>
<div class="seg" role="group" aria-label="Which build's data to show" id="ds">
<button type="button" data-ds="dev" aria-pressed="false">dev today</button>
<button type="button" data-ds="feat" aria-pressed="true">dev + printer-connections branch</button>
</div>
<span class="small muted">Fields tagged <span class="chip chip-info chip-dashed">feature</span> exist only on <code>feature/7efc2030…-printer-connections</code> (4e17f9e), not in production.</span>
</section>
<div class="groups">
<section aria-labelledby="g-progress">
<h2 class="group-h" id="g-progress">In progress <span class="n">1</span></h2>
<div class="pgrid">
<article class="pcard nm-raised" aria-labelledby="p1">
<div class="phead">
<div style="flex:1;min-width:0"><h3 id="p1">P1S - Prints Charming</h3><span class="model">Bambu Lab · P1S</span></div>
<span class="chip chip-accent">● Printing</span>
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for P1S - Prints Charming" aria-haspopup="menu">⋯</button>
</div>
<div class="row small" data-feature><span class="pulse" aria-hidden="true"></span><span>Ready · last report <b>2s ago</b></span><span class="chip chip-info chip-dashed">feature</span></div>
<div class="row small" data-devonly><span class="pulse" aria-hidden="true"></span><span>Connected</span></div>
<div class="cam" role="img" aria-label="Camera snapshot from P1S - Prints Charming"><span class="stamp">snapshot 09:41:02</span></div>
<div class="nowjob nm-inset">
<span class="jn">Gridfinity_bins_3x2</span><span class="num">1h 04m</span>
<span class="small muted">started 08:37 · weight read from the 3MF when it ends</span><span class="small muted">so far</span>
</div>
<div class="slots4" aria-label="Filament loaded in P1S - Prints Charming">
<div class="row"><span class="small"><b>Loaded filament</b></span><span class="chip chip-neutral">not confirmed in use</span></div>
<div class="cont nm-inset">
<div class="cont-h"><b>AMS 1</b><span class="muted">4 / 4</span></div>
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#141414"></i></span><span class="nm">Sunlu · Black <span class="muted">PLA + 2.0</span></span><span class="num">1000 g</span></div>
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#b87333"></i></span><span class="nm">Sunlu · Copper <span class="muted">PLA + Silk</span></span><span class="num" style="color:var(--status-warning)">⚠ 97 g</span></div>
<div class="slotrow"><span class="i">3</span><span class="swatch swatch-sm"><i style="background:#ece8df"></i></span><span class="nm">Overture · Mist Gray <span class="muted">PLA Marble</span></span><span class="num">920 g</span></div>
<div class="slotrow"><span class="i">4</span><span class="swatch swatch-sm"><i style="background:#101010"></i></span><span class="nm">JAYO · Black <span class="muted">Rapid PETG Matte</span></span><span class="num">655 g</span></div>
</div>
<div class="cont nm-inset">
<div class="cont-h"><b>External</b><span class="muted">0 / 1</span></div>
<div class="slotrow empty"><span class="i">1</span><span></span><span class="nm">Empty</span><span></span></div>
</div>
</div>
<details class="cfg"><summary>Connection details (stored)</summary>
<dl class="conn" style="margin-top:8px">
<dt>IP</dt><dd>10.60.1.242</dd><dt>Serial</dt><dd>01P09A••••••4821</dd><dt>Access code</dt><dd>✓ stored, never shown</dd><dt>Added</dt><dd>7 Aug 2026</dd>
<dt data-feature>Session</dt><dd data-feature>#3 <span class="chip chip-info chip-dashed">feature</span></dd>
</dl></details>
<p class="small muted" style="margin:0">Last finished: Lid_v3 · failed yesterday 16:49</p>
</article>
</div>
</section>
<section aria-labelledby="g-attn">
<h2 class="group-h" id="g-attn">Needs attention <span class="n">1</span></h2>
<div class="pgrid">
<article class="pcard nm-raised attn" aria-labelledby="p3">
<div class="phead">
<div style="flex:1;min-width:0"><h3 id="p3">A1 - Jordyns Layer Slayer</h3><span class="model">Bambu Lab · A1</span></div>
<span class="chip chip-warn" data-feature>⟳ Reconnecting</span>
<span class="chip chip-warn" data-devonly>⚡ Offline</span>
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for A1 - Jordyns Layer Slayer" aria-haspopup="menu">⋯</button>
</div>
<div class="lost" role="status">
<h4>⚡ MQTT connection lost</h4>
<span data-feature>Disconnected, reconnecting. Last contact <b>6m ago</b> (09:35:11). <span class="chip chip-info chip-dashed">feature</span></span>
<span data-devonly>Extrudex's MQTT session to this printer dropped. The time it dropped isn't recorded on dev.</span>
<ul class="small">
<li>The printer itself may still be printing. This is Extrudex's connection, not the printer's power.</li>
<li><b>Print capture is paused.</b> A job that starts and finishes during the outage won't be recorded.</li>
<li>On reconnect: if <i>Coaster_set</i> is still running, its record stays open and gets <b>+1 gap</b> (its duration becomes an upper bound). Otherwise it closes as <b>unknown</b>, with no duration.</li>
</ul>
</div>
<div class="nowjob nm-inset stale" style="border:1px dashed var(--status-warning)">
<span class="jn">Coaster_set</span><span class="stale-tag">OPEN RECORD</span>
<span class="small muted">started 08:55 · watched until the connection dropped</span><span></span>
<span class="small" data-feature style="grid-column:1/-1"><span class="stale-tag">Last known:</span> Printing · 6m ago. Cached from the lost session, not live. <span class="chip chip-info chip-dashed">feature</span></span>
</div>
<div class="cam-off">Camera unavailable while the printer is offline</div>
<div class="slots4" aria-label="Filament loaded in A1 - Jordyns Layer Slayer">
<div class="row"><span class="small"><b>Loaded filament</b></span><span class="chip chip-neutral">from Extrudex records, not the printer</span></div>
<div class="cont nm-inset">
<div class="cont-h"><b>AMS Lite</b><span class="muted">2 / 4</span></div>
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#1b1b1b"></i></span><span class="nm">Sunlu · Black <span class="muted">PETG CF Matte</span></span><span class="num">812 g</span></div>
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#e8453c"></i><i style="background:#f2c230"></i><i style="background:#2a8fd6"></i></span><span class="nm">AMOLEN · Red Yellow Blue <span class="muted">PLA Matte</span></span><span class="num">744 g</span></div>
<div class="slotrow empty"><span class="i">3</span><span></span><span class="nm">Empty</span><span></span></div>
<div class="slotrow empty"><span class="i">4</span><span></span><span class="nm">Empty</span><span></span></div>
</div>
</div>
<details class="cfg" open><summary>Connection details (stored)</summary>
<dl class="conn" style="margin-top:8px">
<dt>IP</dt><dd>10.60.1.x <span class="muted">(sample)</span></dd><dt>Serial</dt><dd>039••••••••7710</dd><dt>Access code</dt><dd>✓ stored, never shown</dd>
<dt data-feature>Lifecycle</dt><dd data-feature>reconnecting · <code>stale: true</code> <span class="chip chip-info chip-dashed">feature</span></dd>
<dt data-feature>Failure</dt><dd data-feature><span class="muted">none reported. A reconnect can have no failure detail.</span></dd>
<dt data-feature>Session</dt><dd data-feature>#14 (increments on every reconnect)</dd>
</dl></details>
<div class="actions"><button class="btn btn-sm" type="button">Edit connection</button><span class="small muted">Retrying automatically. There's nothing to press.</span></div>
</article>
</div>
</section>
<section aria-labelledby="g-idle">
<h2 class="group-h" id="g-idle">Idle <span class="n">1</span></h2>
<div class="pgrid">
<article class="pcard nm-raised-sm" aria-labelledby="p2" style="padding: var(--space-5)">
<div class="phead">
<div style="flex:1;min-width:0"><h3 id="p2">H2C - Constipation Orion</h3><span class="model">Bambu Lab · H2C</span></div>
<span class="chip chip-neutral">Idle</span>
<button class="btn btn-icon btn-sm" type="button" aria-label="Actions for H2C - Constipation Orion" aria-haspopup="menu">⋯</button>
</div>
<div class="row small"><span class="pulse" aria-hidden="true"></span><span data-feature>Ready · last report 4s ago <span class="chip chip-info chip-dashed">feature</span></span><span data-devonly>Connected</span></div>
<p class="small" style="margin:0">Last: <b>Benchy_x4</b> · finished 05:27 · <span style="color:var(--status-warning);font-weight:700">≤ 3h 12m</span> <span class="chip chip-warn chip-dashed">upper bound · 1 gap</span></p>
<div class="cont nm-inset">
<div class="cont-h"><b>AMS 1</b><span class="muted">3 / 4</span></div>
<div class="slotrow"><span class="i">1</span><span class="swatch swatch-sm"><i style="background:#141414"></i></span><span class="nm">Elegoo · Black <span class="muted">Rapid PETG</span></span><span class="num" style="color:var(--status-warning)">⚠ 142 g</span></div>
<div class="slotrow"><span class="i">2</span><span class="swatch swatch-sm"><i style="background:#1d4fa8"></i></span><span class="nm">Elegoo · Blue <span class="muted">Rapid PETG</span></span><span class="num">1000 g</span></div>
<div class="slotrow"><span class="i">3</span><span class="swatch swatch-sm"><i style="background:#f4f4f0"></i></span><span class="nm">Elegoo · White <span class="muted">PLA Matte</span></span><span class="num">610 g</span></div>
<div class="slotrow empty"><span class="i">4</span><span></span><span class="nm">Empty</span><span></span></div>
</div>
<details class="cfg"><summary>Connection details (stored)</summary>
<dl class="conn" style="margin-top:8px"><dt>IP</dt><dd>10.60.1.202</dd><dt>Serial</dt><dd>31B••••••••0093</dd><dt>Access code</dt><dd>✓ stored, never shown</dd></dl></details>
</article>
</div>
</section>
</div>
<section class="future" aria-labelledby="future-h">
<h2 id="future-h"><span class="chip chip-neutral chip-dashed">Future, not built</span> Ideas that need data Extrudex doesn't store</h2>
<p class="small muted" style="margin:var(--space-2) 0 0">Nothing in this box can be built from today's data. Each item says what's missing.</p>
<div class="fgrid">
<div class="fitem"><h3>Progress % and time left</h3><div class="track ghost" aria-hidden="true"><span style="width:58%"></span></div>
<span class="why"><code>mc_percent</code> is parsed from MQTT but used only internally. It isn't stored or returned by <code>/api/printers</code>. ETA isn't read at all.</span></div>
<div class="fitem"><h3>Nozzle and bed temperature</h3><span class="ghost small">220 °C · 60 °C</span>
<span class="why">Temperatures aren't parsed from the report.</span></div>
<div class="fitem"><h3>Current layer</h3><span class="ghost small">Layer 112 / 190</span>
<span class="why">Layer fields aren't parsed.</span></div>
<div class="fitem"><h3>AMS remaining % and humidity</h3><span class="ghost small">82% · humidity 2</span>
<span class="why">Not read. AMS <code>remain</code> is only meaningful for genuine RFID spools, which isn't what's mostly run here.</span></div>
<div class="fitem"><h3>Alert when MQTT drops</h3><span class="ghost small">🔔 notify after 5 min</span>
<span class="why">Drops only show in container logs today (CONTEXT open work #4). On dev, nothing stores <i>when</i> it dropped.</span></div>
<div class="fitem"><h3>Which spool is feeding now</h3><span class="ghost small">▶ AMS 1 · slot 2</span>
<span class="why">"Loaded" is recorded; "in use" isn't. That's why the list says <i>not confirmed in use</i>.</span></div>
</div>
</section>
</main>
</div>
<script>
(function(){
function set(v){document.body.classList.toggle('dev-only', v==='dev');
document.querySelectorAll('#ds [data-ds]').forEach(function(b){b.setAttribute('aria-pressed',String(b.getAttribute('data-ds')===v))});}
document.addEventListener('click',function(e){var b=e.target.closest('#ds [data-ds]'); if(b) set(b.getAttribute('data-ds'));});
})();
</script>
</body>
</html>