Operator screen
Mimic
A process mimic with live symbols and pipes. Clicking a symbol opens its faceplate.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Process Mimic - Smart.Industrial</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<div class="hmi-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">PROCESS MIMIC</span>
<span class="hmi-area">Area 2 · feed and reaction</span>
</div>
<smart-alarm-banner id="mimicBanner" class="header-banner" empty-message="Process within limits"></smart-alarm-banner>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</header>
<section class="hmi-body">
<div class="hmi-panel hmi-panel-mimic">
<div class="hmi-panel-header">
<span>Flow diagram</span>
<span class="hmi-panel-note">click an item for detail</span>
</div>
<div class="hmi-panel-content">
<!-- Laid out on a grid: equipment sits in the wide columns, pipe
runs fill the narrow ones between them. Snapping to a grid is
what keeps every connection aligned as the panel resizes. -->
<div class="mimic" id="mimic">
<!-- standby pump, above the duty pump -->
<div class="node" style="grid-column: 5; grid-row: 1;">
<smart-mimic-symbol id="pumpB" symbol="pump" label="P-2101B" label-position="top" data-id="P-2101B"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeStandby" orientation="vertical" style="grid-column: 5; grid-row: 2;"></smart-pipe>
<!-- agitator motor sits above the reactor it drives -->
<div class="node" style="grid-column: 11; grid-row: 1;">
<smart-mimic-symbol id="agitator" symbol="motor" label="AG-2112" label-position="top" data-id="AG-2112"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeShaft" orientation="vertical" style="grid-column: 11; grid-row: 2;"></smart-pipe>
<!-- the main run, left to right -->
<div class="node" style="grid-column: 1; grid-row: 3;">
<smart-mimic-symbol id="tankFeed" symbol="vessel" label="TK-01 FEED" data-id="TK-01"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeSuction" orientation="horizontal" style="grid-column: 2 / 5; grid-row: 3;"></smart-pipe>
<div class="node" style="grid-column: 5; grid-row: 3;">
<smart-mimic-symbol id="pumpA" symbol="pump" label="P-2101A" data-id="P-2101A"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeDischarge" orientation="horizontal" style="grid-column: 6; grid-row: 3;"></smart-pipe>
<div class="node" style="grid-column: 7; grid-row: 3;">
<smart-mimic-symbol id="instrPT" symbol="instrument" label="PT-2101" data-id="PT-2101"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeThrough" orientation="horizontal" style="grid-column: 8; grid-row: 3;"></smart-pipe>
<div class="node" style="grid-column: 9; grid-row: 3;">
<smart-mimic-symbol id="heater" symbol="exchanger" label="HX-2108" data-id="HX-2108"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeToReactor" orientation="horizontal" style="grid-column: 10; grid-row: 3;"></smart-pipe>
<div class="node" style="grid-column: 11; grid-row: 3;">
<smart-mimic-symbol id="tankReactor" symbol="vessel" label="REACTOR" data-id="R-2110"></smart-mimic-symbol>
</div>
<smart-pipe id="pipeProduct" orientation="horizontal" style="grid-column: 12; grid-row: 3;"></smart-pipe>
<div class="node" style="grid-column: 13; grid-row: 3;">
<smart-mimic-symbol id="valve" symbol="valve" label="CV-2140" data-id="CV-2140"></smart-mimic-symbol>
</div>
<!-- recycle, only in service when the reactor runs hot -->
<smart-pipe id="pipeRecycleDown" orientation="vertical" style="grid-column: 11; grid-row: 4;"></smart-pipe>
<smart-pipe id="pipeRecycleCornerB" elbow="bottomLeft" reverse style="grid-column: 11; grid-row: 5;"></smart-pipe>
<smart-pipe id="pipeRecycle" orientation="horizontal" reverse style="grid-column: 6 / 11; grid-row: 5;"></smart-pipe>
<smart-pipe id="pipeRecycleCornerA" elbow="bottomRight" reverse style="grid-column: 5; grid-row: 5;"></smart-pipe>
<smart-pipe id="pipeRecycleUp" orientation="vertical" style="grid-column: 5; grid-row: 4;"></smart-pipe>
</div>
</div>
</div>
<aside class="hmi-panel hmi-panel-detail">
<div class="hmi-panel-header"><span id="detailTitle">Selection</span></div>
<div class="hmi-panel-content">
<p class="detail-hint" id="detailHint">Click an item in the diagram.</p>
<dl class="detail-list" id="detailList"></dl>
<div class="detail-readout" id="detailReadout"></div>
</div>
</aside>
</section>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.mimicsymbol.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.pipe.js"></script>
<script type="module" src="node_modules/smart-industrial/source/smart.industrial.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmbanner.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.sevensegment.js"></script>
<script type="text/javascript" src="data.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* Process mimic screen.
*
* The diagram is assembled from components rather than drawn: smart-mimic-symbol
* for equipment (vessels included), smart-pipe for the runs between them.
* That means updating the screen is setting properties, not patching SVG. */
const ITEMS = {
'TK-01': { name: 'Feed tank', kind: 'Vessel, 12 m³', tags: ['LT-2201'] },
'P-2101A': { name: 'Feed pump A', kind: 'Centrifugal pump, 55 kW', tags: ['PT-2101', 'FT-2104'] },
'P-2101B': { name: 'Feed pump B', kind: 'Centrifugal pump, 55 kW (standby)', tags: ['PT-2101'] },
'HX-2108': { name: 'Feed pre-heater', kind: 'Shell and tube exchanger', tags: ['TT-2107'] },
'R-2110': { name: 'Reactor', kind: 'Stirred vessel, 8 m³', tags: ['LT-2202', 'TT-2108'] },
'AG-2112': { name: 'Reactor agitator', kind: 'Top-entry agitator', tags: ['ST-2112'] },
'CV-2140': { name: 'Discharge valve', kind: 'Control valve', tags: ['CV-2140'] },
'PT-2101': { name: 'Discharge pressure', kind: 'Pressure transmitter', tags: ['PT-2101'] }
};
const TAG_FORMAT = {
'LT-2201': { unit: '%', digits: 1, high: 92, low: 20 },
'LT-2202': { unit: '%', digits: 1, high: 90, low: 18 },
'PT-2101': { unit: 'bar', digits: 2, high: 8.5 },
'FT-2104': { unit: 'L/min', digits: 0, low: 240 },
'TT-2107': { unit: '°C', digits: 1, high: 75 },
'TT-2108': { unit: '°C', digits: 1, high: 118 },
'ST-2112': { unit: 'RPM', digits: 0, high: 5400 },
'CV-2140': { unit: '%', digits: 0 }
};
function format(tag, value) {
const spec = TAG_FORMAT[tag] || { unit: '', digits: 1 };
return value.toFixed(spec.digits) + ' ' + spec.unit;
}
function severityFor(tag, value) {
const spec = TAG_FORMAT[tag];
if (!spec) {
return 'normal';
}
if (spec.high !== undefined && value > spec.high) {
return 'high';
}
if (spec.low !== undefined && value < spec.low) {
return 'low';
}
return 'normal';
}
window.onload = function () {
const banner = document.getElementById('mimicBanner'),
themeToggle = document.getElementById('themeToggle'),
detailTitle = document.getElementById('detailTitle'),
detailHint = document.getElementById('detailHint'),
detailList = document.getElementById('detailList'),
detailReadout = document.getElementById('detailReadout');
/* Vessels are symbols with a level, so a tank on the diagram shows what is
in it rather than borrowing a UI gauge's chrome. */
const tankFeed = document.getElementById('tankFeed'),
tankReactor = document.getElementById('tankReactor');
/* Everything that can be selected, vessels included - the highlight has to follow
the click onto a tank as well as onto a pump. */
const symbols = {
'TK-01': tankFeed,
'R-2110': tankReactor,
'P-2101A': document.getElementById('pumpA'),
'P-2101B': document.getElementById('pumpB'),
'HX-2108': document.getElementById('heater'),
'AG-2112': document.getElementById('agitator'),
'CV-2140': document.getElementById('valve'),
'PT-2101': document.getElementById('instrPT')
};
/* Each run is one or more pipe segments that carry flow together. The
agitator shaft is a mechanical link rather than a line, so it is not in
here - it never "flows". */
const pipes = {
suction: ['pipeSuction'],
discharge: ['pipeDischarge', 'pipeThrough', 'pipeToReactor'],
product: ['pipeProduct'],
recycle: ['pipeRecycleDown', 'pipeRecycleCornerB', 'pipeRecycle', 'pipeRecycleCornerA', 'pipeRecycleUp'],
standby: ['pipeStandby']
};
for (const run in pipes) {
if (Object.prototype.hasOwnProperty.call(pipes, run)) {
pipes[run] = pipes[run].map((id) => document.getElementById(id)).filter(Boolean);
}
}
let selectedId = null;
/* ── Theme ───────────────────────────────────────────────────────── */
function applyTheme(theme) {
document.body.setAttribute('theme', theme);
const elements = document.querySelectorAll('smart-alarm-banner, smart-seven-segment, smart-mimic-symbol, smart-pipe');
for (let i = 0; i < elements.length; i++) {
elements[i].theme = theme;
}
themeToggle.textContent = theme === 'industrial-dark' ? 'Light' : 'Dark';
}
themeToggle.addEventListener('click', function () {
applyTheme(document.body.getAttribute('theme') === 'industrial-dark' ? 'industrial' : 'industrial-dark');
});
/* ── Selection ───────────────────────────────────────────────────── */
function select(id) {
selectedId = id;
for (const key in symbols) {
if (Object.prototype.hasOwnProperty.call(symbols, key)) {
symbols[key].selected = key === id;
}
}
renderDetail(MimicData.snapshot());
}
/* Symbols report their own clicks, so the screen listens for one event
rather than hit-testing the diagram. */
for (const key in symbols) {
if (Object.prototype.hasOwnProperty.call(symbols, key)) {
symbols[key].addEventListener('symbolClick', function (event) {
select(event.target.getAttribute('data-id'));
});
}
}
function renderDetail(state) {
if (!selectedId) {
return;
}
const item = ITEMS[selectedId];
if (!item) {
return;
}
detailHint.style.display = 'none';
detailTitle.textContent = selectedId;
const equipmentState = state.equipment[selectedId];
detailList.innerHTML =
'<dt>Description</dt><dd>' + item.name + '</dd>' +
'<dt>Type</dt><dd>' + item.kind + '</dd>' +
(equipmentState ? '<dt>State</dt><dd id="detailState" class="state-' + equipmentState + '">' + equipmentState + '</dd>' : '');
/* Readouts are created once per selection and then only have their
value set, so switching items does not rebuild them every tick. */
detailReadout.innerHTML = '';
item.tags.forEach(function (tag) {
const spec = TAG_FORMAT[tag] || { unit: '', digits: 1 },
wrapper = document.createElement('div'),
label = document.createElement('span'),
display = document.createElement('smart-seven-segment');
wrapper.className = 'readout-row';
label.className = 'readout-tag';
label.textContent = tag;
display.digits = spec.digits > 0 ? 5 : 4;
display.precisionDigits = spec.digits;
display.unit = spec.unit;
display.theme = document.body.getAttribute('theme');
display.value = state.values[tag];
display.setAttribute('data-tag', tag);
wrapper.appendChild(label);
wrapper.appendChild(display);
detailReadout.appendChild(wrapper);
});
}
/* The per-tick half of the detail panel: the equipment state line. The readouts are
refreshed in updateDiagram, so nothing here is rebuilt - calling renderDetail on
every tick, as this screen once did, threw the seven-segment displays away and
recreated them once a second, which is the flicker the comment above forbids. */
function refreshDetail(state) {
const detailState = document.getElementById('detailState'),
equipmentState = selectedId ? state.equipment[selectedId] : null;
if (detailState && equipmentState) {
detailState.textContent = equipmentState;
detailState.className = 'state-' + equipmentState;
}
}
/* ── Diagram updates ─────────────────────────────────────────────── */
function updateDiagram(state) {
/* Equipment state is a property on the symbol. */
for (const id in state.equipment) {
if (Object.prototype.hasOwnProperty.call(state.equipment, id) && symbols[id]) {
symbols[id].state = state.equipment[id];
}
}
/* The instrument bubble carries its own reading. */
symbols['PT-2101'].value = format('PT-2101', state.values['PT-2101']);
symbols['CV-2140'].value = format('CV-2140', state.values['CV-2140']);
symbols['HX-2108'].value = format('TT-2107', state.values['TT-2107']);
symbols['AG-2112'].value = format('ST-2112', state.values['ST-2112']);
/* Vessels show their contents. */
tankFeed.level = state.values['LT-2201'];
tankFeed.value = format('LT-2201', state.values['LT-2201']);
tankReactor.level = state.values['LT-2202'];
tankReactor.value = format('LT-2202', state.values['LT-2202']);
/* Pipes know whether they are flowing. The standby leg only carries
flow when the standby pump is actually running. */
const flowByRun = {
suction: state.flow.suction,
discharge: state.flow.discharge,
product: state.flow.product,
recycle: state.flow.recycle,
standby: state.equipment['P-2101B'] === 'running'
};
for (const run in pipes) {
if (Object.prototype.hasOwnProperty.call(pipes, run)) {
pipes[run].forEach(function (pipe) {
pipe.flowing = !!flowByRun[run];
});
}
}
Array.prototype.forEach.call(detailReadout.querySelectorAll('smart-seven-segment'), function (display) {
display.value = state.values[display.getAttribute('data-tag')];
});
}
/* ── Alarms derived from the same values ─────────────────────────── */
/* When each tag first went out of limits. An alarm's time is the moment it was
raised, not the moment the screen last looked - stamping `new Date()` on every
tick had the banner's clock advancing by the second on a standing alarm. */
const raisedAt = {};
function updateBanner(state) {
const alarms = [];
for (const tag in state.values) {
if (!Object.prototype.hasOwnProperty.call(state.values, tag)) {
continue;
}
const severity = severityFor(tag, state.values[tag]);
if (severity === 'normal') {
delete raisedAt[tag];
continue;
}
if (!raisedAt[tag]) {
raisedAt[tag] = new Date();
}
alarms.push({
id: tag,
tag: tag,
message: (severity === 'high' ? 'above high limit' : 'below low limit') +
', ' + format(tag, state.values[tag]),
priority: tag === 'TT-2108' ? 1 : 2,
timestamp: raisedAt[tag],
acknowledged: false
});
}
banner.alarms = alarms;
}
applyTheme('industrial-dark');
MimicData.subscribe(function (state) {
updateDiagram(state);
updateBanner(state);
refreshDetail(state);
});
MimicData.start(1000);
select('AG-2112');
};
styles.css
/* Process mimic screen. The diagram takes its colours from the same industrial
tokens as the rest of the suite, so it follows the theme. */
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 13px;
overflow: hidden;
}
.hmi-screen {
display: grid;
grid-template-rows: auto 1fr;
gap: 10px;
padding: 10px;
height: 100%;
box-sizing: border-box;
}
/* ── Header ──────────────────────────────────────────────────────────── */
.hmi-header {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.hmi-identity {
display: flex;
flex-direction: column;
gap: 2px;
flex: none;
}
.hmi-plant {
font-size: 16px;
font-weight: 600;
letter-spacing: .08em;
}
.hmi-area {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .04em;
}
.header-banner {
flex: 1;
min-width: 0;
}
.hmi-theme-toggle {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
flex: none;
font-family: inherit;
font-size: 11px;
letter-spacing: .06em;
padding: 5px 12px;
text-transform: uppercase;
}
.hmi-theme-toggle:hover {
background: var(--smart-ui-state-hover);
border-color: var(--smart-ui-state-border-hover);
color: var(--smart-ui-state-color-hover);
}
/* ── Body ────────────────────────────────────────────────────────────── */
.hmi-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 340px);
gap: 10px;
min-height: 0;
}
.hmi-panel {
display: flex;
flex-direction: column;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
overflow: hidden;
min-width: 0;
min-height: 0;
}
.hmi-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--smart-industrial-panel-header-background);
border-bottom: 1px solid var(--smart-industrial-panel-border);
color: var(--smart-text-secondary-color);
font-size: 11.5px;
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
}
.hmi-panel-note {
color: var(--smart-text-tertiary-color);
letter-spacing: 0;
text-transform: none;
}
.hmi-panel-content {
flex: 1;
min-height: 0;
padding: 10px;
overflow: auto;
}
/* ── The diagram ─────────────────────────────────────────────────────────
A grid: equipment sits in the wide columns, pipe runs fill the narrow ones
between them. Snapping to a grid is what keeps every connection aligned as
the panel resizes - the alternative is hand-tuned offsets that drift. */
.mimic {
display: grid;
grid-template-columns:
minmax(90px, 1.1fr) minmax(24px, .5fr) 0 0
minmax(76px, .9fr) minmax(24px, .5fr)
minmax(76px, .9fr) minmax(24px, .5fr)
minmax(76px, .9fr) minmax(24px, .5fr)
minmax(92px, 1.1fr) minmax(24px, .5fr)
minmax(76px, .9fr);
grid-template-rows: auto 44px auto 44px auto;
align-items: center;
justify-items: center;
/* Rows size to their content and the block centres in the panel, rather
than auto rows stretching to fill and pulling the run apart. */
align-content: center;
gap: 0;
width: 100%;
height: 100%;
min-height: 430px;
padding: 10px 4px;
box-sizing: border-box;
}
.node {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.node-tag {
color: var(--smart-text-secondary-color);
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
letter-spacing: .04em;
white-space: nowrap;
}
/* Pipes stretch to fill their cell so runs meet the equipment either side. */
/* A fixed segment height: height:100% in an auto grid row makes that row
absorb all the free space and pulls the diagram apart. */
smart-pipe {
width: 100%;
height: 22px;
}
/* A vertical run fills its fixed-height row and is as wide as a horizontal one is tall,
so the two meet at the same centreline. */
smart-pipe[orientation="vertical"] {
width: 22px;
height: 100%;
justify-self: center;
align-self: stretch;
}
/* A corner is as tall as the run it turns into, so its vertical leg meets the pipe above. */
smart-pipe[elbow] {
height: 22px;
}
smart-mimic-symbol {
--smart-mimic-symbol-size: 62px;
}
/* The tag and the reading hang below the glyph without adding to its box, so every
glyph's centre sits on the row's centreline and the pipes meet the equipment rather
than its caption. A caption has the canvas behind it, so a vertical run passing it is
cut by the text, as a line is cut by a label on a drawing. */
.mimic smart-mimic-symbol .smart-container {
position: relative;
}
.mimic smart-mimic-symbol .smart-mimic-label,
.mimic smart-mimic-symbol .smart-mimic-value {
position: absolute;
left: 50%;
transform: translateX(-50%);
padding: 0 4px;
background: var(--smart-industrial-panel-header-background, var(--smart-base-200));
z-index: 1;
}
.mimic smart-mimic-symbol .smart-mimic-label {
top: calc(100% + 4px);
}
.mimic smart-mimic-symbol .smart-mimic-value {
top: calc(100% + 20px);
}
.mimic smart-mimic-symbol[label-position="top"] .smart-mimic-label {
top: auto;
bottom: calc(100% + 4px);
}
.mimic smart-mimic-symbol[label-position="top"] .smart-mimic-value {
top: auto;
bottom: calc(100% + 20px);
}
/* Vessels are taller than they are wide, so they get their own box. */
smart-mimic-symbol[symbol="vessel"] {
--smart-mimic-symbol-size: 92px;
}
/* ── Detail panel ────────────────────────────────────────────────────── */
.detail-hint {
color: var(--smart-text-tertiary-color);
margin: 0;
}
.detail-list {
margin: 0 0 16px;
}
.detail-list dt {
color: var(--smart-text-tertiary-color);
font-size: 10px;
letter-spacing: .06em;
text-transform: uppercase;
margin-top: 10px;
}
.detail-list dd {
margin: 3px 0 0;
}
.detail-list dd.state-running {
color: var(--smart-industrial-running);
}
.detail-list dd.state-stopped {
color: var(--smart-industrial-stopped);
}
.detail-list dd.state-manual {
color: var(--smart-industrial-manual);
}
.detail-list dd.state-fault {
color: var(--smart-industrial-alarm-critical-text, var(--smart-industrial-alarm-critical));
}
.readout-row {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 12px;
}
.readout-tag {
color: var(--smart-text-tertiary-color);
font-family: Consolas, "Courier New", monospace;
font-size: 11px;
}
.readout-row smart-seven-segment {
--smart-seven-segment-height: 34px;
}
/* ── Responsive ──────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
body.viewport {
overflow: auto;
}
.hmi-body {
grid-template-columns: 1fr;
}
}