/* The stylesheets' entry point: site-api joins these @imports into one file, the fallback index.html loads
   them as they are. The order matters. */
/* fonts.css */
/* Self-hosted fonts (SIL Open Font License 1.1), see ../fonts/LICENSE.md. Subsets: cyrillic, latin, latin-ext. */
/* cyrillic */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/caveat-cyrillic.woff2?v=6276a2f5a5) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/caveat-latin-ext.woff2?v=f626b95c1e) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(/assets/fonts/caveat-latin.woff2?v=891951df5e) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/caveat-cyrillic.woff2?v=6276a2f5a5) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/caveat-latin-ext.woff2?v=f626b95c1e) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(/assets/fonts/caveat-latin.woff2?v=891951df5e) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/handjet-cyrillic.woff2?v=b3a44576b7) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/handjet-latin-ext.woff2?v=1083291993) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(/assets/fonts/handjet-latin.woff2?v=dd94270aaf) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/handjet-cyrillic.woff2?v=b3a44576b7) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/handjet-latin-ext.woff2?v=1083291993) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Handjet';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(/assets/fonts/handjet-latin.woff2?v=dd94270aaf) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-cyrillic.woff2?v=9409ee2181) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin-ext.woff2?v=d160e20920) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin.woff2?v=e2291e842c) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-cyrillic.woff2?v=9409ee2181) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin-ext.woff2?v=d160e20920) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin.woff2?v=e2291e842c) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-cyrillic.woff2?v=9409ee2181) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin-ext.woff2?v=d160e20920) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/ibm-plex-sans-latin.woff2?v=e2291e842c) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-cyrillic.woff2?v=717684b09f) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin-ext.woff2?v=a6d4d0a825) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 400;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin.woff2?v=2c4e3f0723) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-cyrillic.woff2?v=717684b09f) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin-ext.woff2?v=a6d4d0a825) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 500;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin.woff2?v=2c4e3f0723) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-cyrillic.woff2?v=717684b09f) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* latin-ext */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin-ext.woff2?v=a6d4d0a825) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
  font-family: 'Tektur';
  font-style: normal;
  font-weight: 700;
  font-stretch: 100%;
  font-display: swap;
  src: url(/assets/fonts/tektur-latin.woff2?v=2c4e3f0723) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* tokens.css */
/* Tokens. Dark by default (a black rack), light is beige 90s hardware.
   The theme is one switch: --light is 0 or 1 (--L the same in %), set below by the system or by data-theme on
   <html> (theme.js). A value the themes differ in is written once: a colour as
   color-mix(in srgb, LIGHT var(--L), DARK), a number as calc(LIGHT * var(--light) + DARK * (1 - var(--light))).
   tools/check-themes.mjs (CI) keeps the switch here.
   LCD colours come in -d/-l pairs; site-api puts the cover's pair into <style id="cover-palette">. A cover's
   screen need not follow the theme, so --lcd colours only the screen's background. */
@property --lcd { syntax: "<color>"; inherits: true; initial-value: #ffb547; }
@property --lcd-bg { syntax: "<color>"; inherits: true; initial-value: #1b1306; }
@property --lcd-dim { syntax: "<color>"; inherits: true; initial-value: #a87a35; }

/* The switch. color-scheme and the torch's blend mode are keywords: they go with it. */
:root { --light: 0; color-scheme: dark; --beam-blend: screen; }
@media (prefers-color-scheme: light) { :root { --light: 1; color-scheme: light; --beam-blend: multiply; } }
:root[data-theme="dark"] { --light: 0; color-scheme: dark; --beam-blend: screen; }
:root[data-theme="light"] { --light: 1; color-scheme: light; --beam-blend: multiply; }

:root {
  --L: calc(var(--light) * 100%);
  transition: --lcd .8s, --lcd-bg .8s, --lcd-dim .8s;
  --room: color-mix(in srgb, #d8d2c4 var(--L), #121315);
  --void: color-mix(in srgb, #a49d8e var(--L), #0a0b0c);
  --rail: color-mix(in srgb, #bdb6a6 var(--L), #2b2e33);
  --rail-hi: color-mix(in srgb, #d0caba var(--L), #3b3f46);
  --hole: color-mix(in srgb, #5d584e var(--L), #060707);
  --num: color-mix(in srgb, #5f5a50 var(--L), #7d8188);
  --panel: color-mix(in srgb, #e9e3d3 var(--L), #1d1f23);
  --panel-hi: color-mix(in srgb, #f6f1e5 var(--L), #2a2d33);
  --edge: color-mix(in srgb, #aaa392 var(--L), #0b0c0e);
  --ink: color-mix(in srgb, #2b2925 var(--L), #ebe8e2);
  --dim: color-mix(in srgb, #6c675d var(--L), #8f9299);
  --lcd-bg-d: #1b1306; --lcd-d: #ffb547; --lcd-dim-d: #a87a35;
  --lcd-bg-l: #a3b18c; --lcd-l: #1d2715; --lcd-dim-l: #344029;
  --lcd-bg: color-mix(in srgb, var(--lcd-bg-l) var(--L), var(--lcd-bg-d));
  --lcd: color-mix(in srgb, var(--lcd-l) var(--L), var(--lcd-d));
  --lcd-dim: color-mix(in srgb, var(--lcd-dim-l) var(--L), var(--lcd-dim-d));
  --led-ok: #4ee483;
  --led-warn: #ffb547;
  --led-err: #ff5a4e;
  --led-off: color-mix(in srgb, #c9c2b1 var(--L), #33363b);
  --accent: color-mix(in srgb, #7438bb var(--L), #b58cf0);
  --screw: color-mix(in srgb, #8f8b82 var(--L), #8b8f96);
  --screw-hi: color-mix(in srgb, #f2efe8 var(--L), #c9ccd1);
  --key: color-mix(in srgb, #d9d2c0 var(--L), #2a2d32);
  --key-ink: color-mix(in srgb, #2b2925 var(--L), #e9e6df);
  --tape-k: #161616;
  --tape-r: #b8322b;
  --tape-b: #2456a6;
  --tape-ink: #f3f1ec;
  --note-bg: #fff07a;
  --note-fg: #3b331c;
  /* The KVM's screen in the LCDs' colours (the cover's, changing with them): the palette keeps its text at contrast 7,
     the dim one at 4.5, whichever way round it is (a white cover makes a light screen in the dark theme, a black one
     a dark screen in the light theme), so the screen takes them as they are. */
  --crt: var(--lcd-bg);
  --crt-ink: var(--lcd);
  --crt-dim: var(--lcd-dim);
  --crt-faint: color-mix(in srgb, var(--lcd-dim) 60%, var(--crt));
  --crt-rule: color-mix(in srgb, var(--lcd-dim) 30%, transparent);
  --crt-shade: color-mix(in srgb, #3a40302e var(--L), #00000099);
  /* The sign of the rack we are at, lit: white in the dark hall, in the light one the accent. */
  --sign-on: color-mix(in srgb, #7438bb var(--L), #f1efe8);
  --sign-on-ink: color-mix(in srgb, #fbf8f1 var(--L), #15171a);
  --U: 44px;
  --rail-w: 34px;
  --ear: 20px;
  --f-silk: "Tektur", "Bahnschrift", "Arial Narrow", sans-serif;
  --f-lcd: "Handjet", "Consolas", monospace;
  --f-text: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --f-hand: "Caveat", "Segoe Print", cursive;
}
@media (prefers-reduced-motion: reduce) { :root { transition: none; } }
@media (max-width: 560px) {
  :root { --U: 40px; --rail-w: 26px; --ear: 16px; }
}

/* base.css */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: radial-gradient(120% 70% at 50% 0%, var(--panel-hi), var(--room) 60%) fixed, var(--room);
  color: var(--ink);
  font: 400 15px/1.5 var(--f-text);
}
/* Links in running text: the text's colour and a faint underline; hardware links (ports, keys, signs) style
   themselves. */
a {
  color: inherit; text-decoration-line: underline; text-decoration-thickness: 1px; text-underline-offset: .2em;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent); transition: text-decoration-color .15s;
}
a:hover { text-decoration-color: currentColor; }
/* Buttons and keys answer every tap: no double-tap zoom on them to swallow the second of two quick taps. */
button, .key, summary { touch-action: manipulation; }
a:focus-visible, button:focus-visible, textarea:focus-visible, input:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.room { position: relative; overflow-x: clip; padding: 10px 16px 170px; min-height: 100vh; min-height: 100dvh; }
[hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@media (max-width: 560px) {
  .room { padding-inline: 8px; }
}

/* hall.css */
/* The server room around the rack, decoration only. The light is .room[data-light]: day, evening, night
   (by the site's time zone), battery. */
.room > .rack { z-index: 1; }
.hall {
  --hall-bg: color-mix(in srgb, #d3cec3 var(--L), #0e1012);
  --hall-wall: color-mix(in srgb, #e8e4db var(--L), #15181b);
  --tile: color-mix(in srgb, #d9d4c9 var(--L), #1c1f23);
  --tile-line: color-mix(in srgb, #b9b2a3 var(--L), #0a0b0d);
  --hole-c: color-mix(in srgb, #b4ad9e var(--L), #07080a);
  --tray: color-mix(in srgb, #a8a191 var(--L), #3a3e45);
  --duct: color-mix(in srgb, #d9b73f var(--L), #d8b23a);
  --lamp: color-mix(in srgb, #fffaf0 var(--L), #eaf2ff);
  --lamp-on: 1;
  --cold: #3d7bd9;
  --cold-a: calc(.12 * var(--light) + .22 * (1 - var(--light)));
  --nb-dim: calc(.97 * var(--light) + .55 * (1 - var(--light)));
  /* The neighbour racks: rails, body, units, blanks, the highlight on a unit's edge. */
  --nb-rail: color-mix(in srgb, #b8b1a1 var(--L), #2b2e33);
  --nb-body: color-mix(in srgb, #c9c2b2 var(--L), #0b0c0e);
  --nb-edge: color-mix(in srgb, #9e9686 var(--L), #000);
  --nu-a: color-mix(in srgb, #efebe2 var(--L), #24272c);
  --nu-b: color-mix(in srgb, #e0d9cb var(--L), #1b1d21);
  --nu-blank: color-mix(in srgb, #bdb6a6 var(--L), #101113);
  --nu-hi: color-mix(in srgb, #fff9 var(--L), #ffffff10);
  position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden;
  background: linear-gradient(var(--hall-wall), var(--hall-bg) 70%);
  transition: filter .8s, background .8s;
}
.hall > * { position: absolute; }

/* The ceiling: a ladder cable tray, the yellow fibre duct, the lights. */
.hall .ceiling { inset: 0 0 auto; height: 110px; }
.hall .tray {
  left: -5%; right: -5%; top: 18px; height: 16px;
  background:
    linear-gradient(var(--tray) 0 3px, transparent 3px calc(100% - 3px), var(--tray) 0) ,
    repeating-linear-gradient(90deg, var(--tray) 0 3px, transparent 3px 26px);
  opacity: .8;
}
.hall .duct { left: -5%; right: -5%; top: 44px; height: 9px; border-radius: 4px; background: var(--duct); opacity: .55; box-shadow: 0 2px 0 #0006; }
.hall .lamp {
  top: 62px; width: 180px; height: 6px; border-radius: 3px; background: var(--lamp);
  opacity: calc(.9 * var(--lamp-on)); box-shadow: 0 0 24px 6px color-mix(in srgb, var(--lamp) 50%, transparent);
}
.hall .lamp.a { left: 6%; } .hall .lamp.b { left: calc(50% - 90px); } .hall .lamp.c { right: 6%; }
/* The camera along the row (--cam: the rack's number): each rack sees its own stretch of the hall (aisle.css). */
.hall { --cam-x: calc((var(--cam, 1) - 1) * -6vw); }
.hall .lamp { translate: var(--cam-x) 0; }
.hall .ceiling::after { /* the lamps' light on the walls */
  content: ""; position: absolute; inset: 60px 0 -500px;
  background: radial-gradient(60% 420px at 50% 0, color-mix(in srgb, var(--lamp) 14%, transparent), transparent);
  opacity: var(--lamp-on);
}

/* Neighbour racks: their status lamps are the real Gatus checks (data-check). */
.hall .nb { top: 70px; bottom: 150px; width: 1000px; filter: brightness(var(--nb-dim)) blur(.6px); }
.hall .nb.l { right: calc(50% + 500px + 28px); }
.hall .nb.r { left: calc(50% + 500px + 28px); }
/* Needed only while the row moves (aisle.css, .hop). */
.hall .nb.ll { right: calc(50% + 500px + 28px + 1028px); }
.hall .nb.rr { left: calc(50% + 500px + 28px + 1028px); }
.hall .nb.c { left: calc(50% - 500px); }
.hall .nb.ll, .hall .nb.c, .hall .nb.rr { visibility: hidden; }
.hall .nbr {
  --slot: color-mix(in srgb, var(--nu-b) 45%, #000);      /* the dark of a bay, a port, a vent */
  --metal: color-mix(in srgb, var(--nu-a) 80%, #fff 6%);  /* drive trays, handles */
  position: relative; height: 100%; border-radius: 6px;
  /* Rails with their square holes, the PDU's strip on the right rail. */
  background:
    repeating-linear-gradient(transparent 0 8px, var(--nb-edge) 8px 13px, transparent 13px 15px) 12px 0 / 10px 100% no-repeat,
    repeating-linear-gradient(transparent 0 8px, var(--nb-edge) 8px 13px, transparent 13px 15px) calc(100% - 12px) 0 / 10px 100% no-repeat,
    linear-gradient(90deg, var(--nb-rail) 0 34px, transparent 34px calc(100% - 34px), var(--nb-rail) 0),
    var(--nb-body);
  box-shadow: inset 0 0 0 1px var(--nb-edge), 0 20px 40px -10px #000;
  display: flex; flex-direction: column; gap: 10px; padding: 26px 34px;
}
.hall .stack { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; gap: 2px; }
/* One unit: a 1U server by default — two ears with screws, four drive trays, a vent grille, its status lamp. */
.hall .hw {
  position: relative; flex: none; height: 42px; border-radius: 2px;
  background:
    repeating-linear-gradient(90deg, var(--slot) 0 2px, transparent 2px 5px) right 70px center / 180px 16px no-repeat,
    repeating-linear-gradient(90deg, transparent 0 8px, var(--slot) 8px 44px, transparent 44px 90px) 64px calc(50% + 8px) / 360px 3px no-repeat,
    repeating-linear-gradient(90deg, var(--metal) 0 86px, var(--slot) 86px 90px) 64px center / 360px 26px no-repeat,
    linear-gradient(var(--nu-a), var(--nu-b));
  box-shadow: inset 0 1px 0 var(--nu-hi), inset 0 -1px 0 #0006;
}
.hall .hw::before, .hall .hw::after { /* the ears */
  content: ""; position: absolute; top: 0; bottom: 0; width: 18px;
  background: radial-gradient(circle at 50% 30%, var(--nb-edge) 0 2.5px, transparent 3px), radial-gradient(circle at 50% 72%, var(--nb-edge) 0 2.5px, transparent 3px), var(--nb-rail);
}
.hall .hw::before { left: 0; } .hall .hw::after { right: 0; }
.hall .hw > i {
  position: absolute; left: 34px; top: 50%; width: 7px; height: 7px; margin-top: -3.5px; border-radius: 50%;
  background: var(--led-ok); box-shadow: 0 0 8px var(--led-ok), 12px 0 0 -1px var(--slot);
}
/* The racks on the left show the viewer their right side: their lamps sit there. */
.hall .nb:is(.l, .ll) .hw > i { left: auto; right: 34px; }
.hall .hw > i.warn { background: var(--led-warn); box-shadow: 0 0 8px var(--led-warn), 12px 0 0 -1px var(--slot); }
.hall .hw > i.err { background: var(--led-err); box-shadow: 0 0 10px var(--led-err), 12px 0 0 -1px var(--slot); }
.hall .hw > i.off { background: var(--led-off); box-shadow: none; }
/* 2U servers: twelve trays in two rows. */
.hall .stack > .hw:nth-child(5n+1) {
  height: 86px;
  background:
    repeating-linear-gradient(90deg, transparent 0 6px, var(--slot) 6px 30px, transparent 30px 62px) 64px 34px / 372px 3px no-repeat,
    repeating-linear-gradient(90deg, transparent 0 6px, var(--slot) 6px 30px, transparent 30px 62px) 64px 68px / 372px 3px no-repeat,
    repeating-linear-gradient(90deg, var(--metal) 0 58px, var(--slot) 58px 62px) 64px 12px / 372px 28px no-repeat,
    repeating-linear-gradient(90deg, var(--metal) 0 58px, var(--slot) 58px 62px) 64px 46px / 372px 28px no-repeat,
    repeating-linear-gradient(var(--slot) 0 2px, transparent 2px 6px) right 60px center / 200px 60px no-repeat,
    linear-gradient(var(--nu-a), var(--nu-b));
}
/* Storage, 2U: twenty-four small bays. */
.hall .stack > .hw:nth-child(7n+4) {
  height: 86px;
  background:
    radial-gradient(circle, var(--slot) 2px, transparent 2.5px) 64px 30px / 32px 36px repeat-x,
    repeating-linear-gradient(90deg, var(--metal) 0 28px, var(--slot) 28px 32px) 64px 10px / 768px 30px no-repeat,
    repeating-linear-gradient(90deg, var(--metal) 0 28px, var(--slot) 28px 32px) 64px 46px / 768px 30px no-repeat,
    linear-gradient(var(--nu-a), var(--nu-b));
}
/* Switches, 1U: two rows of ports. */
.hall .stack > .hw:nth-child(6n+3), .hall .hw.tor {
  background:
    repeating-linear-gradient(90deg, var(--slot) 0 12px, transparent 12px 16px) 120px 7px / 384px 11px no-repeat,
    repeating-linear-gradient(90deg, var(--slot) 0 12px, transparent 12px 16px) 120px 24px / 384px 11px no-repeat,
    linear-gradient(90deg, var(--slot) 0 22px, transparent 22px 30px, var(--slot) 30px 52px) right 60px center / 52px 14px no-repeat,
    linear-gradient(var(--nu-a), var(--nu-b));
}
/* Blanks with a perforated vent, here and there. */
.hall .stack > .hw:nth-child(9n+7) {
  background: radial-gradient(circle, var(--slot) 1.6px, transparent 2px) 0 0 / 7px 7px, var(--nu-blank);
  background-clip: content-box; padding: 6px 28px;
}
/* The patch panel on top: a row of jacks. */
.hall .hw.patch {
  background: repeating-linear-gradient(90deg, var(--slot) 0 14px, transparent 14px 22px) 60px center / 704px 12px no-repeat, linear-gradient(var(--nu-a), var(--nu-b));
}
/* The UPS at the bottom, 2U: its little screen and a vent. */
.hall .hw.ups {
  height: 86px;
  background:
    linear-gradient(color-mix(in srgb, var(--lcd-bg-d) 70%, #000), var(--lcd-bg-d)) 70px center / 120px 38px no-repeat,
    repeating-linear-gradient(90deg, var(--slot) 0 3px, transparent 3px 7px) right 70px center / 260px 46px no-repeat,
    linear-gradient(var(--nu-a), var(--nu-b));
}
/* A check ran (live.js): its lamp blinks off. A neighbour rack without power (power.js): lamps off. */
.hall .hw > i.tx { opacity: .15; box-shadow: none; } /* a check ran on that service (live.js) */
.hall .nb.dark .hw > i { background: var(--led-off) !important; box-shadow: none !important; animation: none !important; }
.hall .nb.dark .nbr { filter: brightness(.8); }

/* The raised floor under the rack: tiles and the cold aisle's perforation. */
.hall .floor { inset: auto 0 0; height: 150px; }
.hall .floor::before {
  content: ""; position: absolute; inset: 0;
  background:
    radial-gradient(circle at 50% 50%, var(--hole-c) 0 1.6px, transparent 2px) 0 0 / 10px 10px,
    linear-gradient(90deg, var(--tile-line) 0 2px, transparent 2px) 0 0 / 60px 60px,
    linear-gradient(var(--tile-line) 0 2px, transparent 2px) 0 0 / 60px 60px,
    var(--tile);
}
.hall .vent { inset: 0; background: radial-gradient(50% 100% at 50% 100%, color-mix(in srgb, var(--cold) calc(var(--cold-a) * 100%), transparent), transparent); }

/* ---------- light: the same in either theme ---------- */
.room[data-light="evening"] .hall { --lamp: #ffd9a8; --lamp-on: .7; filter: brightness(.85); }
.room[data-light="night"] .hall { --lamp-on: 0; --cold-a: .35; --nb-dim: .45; filter: brightness(.75); }
.room[data-light="battery"] .hall { --lamp-on: 0; --cold-a: .1; }
.room[data-light="battery"] .hall::after {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(70% 60% at 50% 0, #ff2a1a40, transparent 70%), #ff1a0a14;
}
@media (prefers-reduced-motion: no-preference) {
  .room[data-light="battery"] .hall::after { animation: alarm 2.4s ease-in-out infinite; }
}
@keyframes alarm { 50% { opacity: .35; } }
/* The UPS's button turned the rack off: the hall is dark. */
.room:has(.rack.powered-off) .hall > * { opacity: .06; transition: opacity .6s; }

/* ---------- narrow: no neighbours in sight ---------- */
@media (max-width: 1100px) { .hall .nb { filter: brightness(calc(var(--nb-dim) * .8)) blur(1.6px); } }
@media (max-width: 760px) {
  .hall .nb, .hall .lamp.a, .hall .lamp.c { display: none; }
  .hall .floor { height: 110px; }
}

/* ---------- the aisle in perspective: the floor and the ceiling go into the distance ---------- */
.hall .floor { perspective: 420px; }
.hall .floor::before {
  inset: -60% -60% 0; transform: rotateX(64deg) translateX(var(--cam-x)); transform-origin: 50% 100%;
}
/* The ceiling in perspective. The .mark lines are invisible points on the yellow duct's axis: app.js leads
   the rack's fibre into the duct by them. */
.hall .ceiling { perspective: 420px; height: 150px; overflow: hidden; }
.hall .tray, .hall .duct { display: none; }
.hall .ceiling .plane {
  position: absolute; inset: 0 -60% -260%; transform: rotateX(-66deg) translateX(var(--cam-x)); transform-origin: 50% 0;
  background:
    linear-gradient(90deg, transparent calc(41% - 6px), var(--duct) 0 calc(41% + 6px), transparent 0),
    linear-gradient(90deg, transparent calc(30% - 22px), var(--tray) 0 calc(30% - 18px), transparent 0 calc(30% + 18px), var(--tray) 0 calc(30% + 22px), transparent 0),
    linear-gradient(90deg, transparent calc(70% - 22px), var(--tray) 0 calc(70% - 18px), transparent 0 calc(70% + 18px), var(--tray) 0 calc(70% + 22px), transparent 0),
    linear-gradient(90deg, var(--tile-line) 0 2px, transparent 2px) 0 0 / 80px 80px,
    linear-gradient(var(--tile-line) 0 2px, transparent 2px) 0 0 / 80px 80px,
    var(--hall-wall);
  -webkit-mask: linear-gradient(#000 40%, transparent);
          mask: linear-gradient(#000 40%, transparent);
  opacity: .9;
}
.hall .ceiling .mark { position: absolute; left: 41%; top: calc(var(--k) * 3%); width: 1px; height: 1px; }

/* aisle.css */
/* The aisle: the signs and the arrows (templates/aisle.html). With JS the rack changes without a reload
   (aisle.js), without it plain links and View Transitions between pages. */
.aisle {
  position: relative; z-index: 5; max-width: 1000px; margin: 0 auto 12px;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.aisle .signs { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; list-style: none; margin: 0; padding: 0; }
.aisle a { text-decoration: none; }
.aisle .signs a {
  position: relative; display: inline-flex; align-items: baseline; gap: 6px; min-height: 30px; padding: 6px 11px;
  font: 500 11px/1 var(--f-silk); letter-spacing: .12em; text-transform: uppercase; color: var(--dim);
  background: var(--panel); border-radius: 3px; box-shadow: inset 0 0 0 1px var(--edge), 0 6px 14px -8px #000;
}
.aisle .signs b { font-weight: 700; color: var(--ink); }
.aisle .signs a[aria-current] { color: var(--ink); background: var(--panel-hi); box-shadow: inset 0 0 0 1px var(--accent), 0 0 18px -6px var(--accent); }
.aisle .signs a.stub { opacity: .7; }
.aisle .step {
  display: inline-flex; align-items: center; gap: 8px; min-height: 36px; min-width: 36px; padding: 0 10px;
  font: 500 11px/1 var(--f-silk); letter-spacing: .12em; text-transform: uppercase; color: var(--dim);
}
.aisle .step i { font: 400 28px/1 var(--f-text); font-style: normal; color: var(--ink); }
.aisle a.step:hover, .aisle .signs a:hover { color: var(--ink); }
.aisle .step.end { visibility: hidden; }
/* The hall's light switch and torch hang in the top right corner (power.css, up to 133px from the edge): where the
   row's arrow would reach them, the aisle keeps clear. A phone without a mouse has neither. */
@media (max-width: 1300px) { .aisle { padding-right: 140px; } }
@media (max-width: 760px) { .aisle { padding-right: 130px; } }
@media (max-width: 560px) { .aisle { padding-right: 0; } }
@media (max-width: 560px) and (hover: hover) { .aisle { padding-right: 70px; } }
@media (max-width: 560px) {
  .aisle { gap: 4px; margin-bottom: 8px; }
  .aisle .signs { gap: 5px; }
  .aisle .signs a { padding: 6px 8px; }
  .aisle .signs a:not([aria-current]) span, .aisle .step span { display: none; }
  .aisle .step { padding: 0 6px; }
}

/* The signs hang from the ceiling on chains, the current one lit. */
.aisle .signs a::before {
  content: ""; position: absolute; left: 18%; right: 18%; bottom: 100%; height: 60px;
  border-inline: 1px dashed #5a5f67; pointer-events: none;
}
.aisle { padding-top: 34px; }
.aisle .signs a[aria-current] {
  background: var(--sign-on); color: var(--sign-on-ink); box-shadow: 0 0 22px -2px color-mix(in srgb, var(--sign-on) 67%, transparent);
}
.aisle .signs a[aria-current] b { color: var(--sign-on-ink); }

/* ---------- the move between pages: without JS and on a plain load ---------- */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  .rack { view-transition-name: rack; }
  .aisle { view-transition-name: aisle; }
  ::view-transition-old(rack) { animation: rack-out .5s cubic-bezier(.5, 0, .3, 1) both; }
  ::view-transition-new(rack) { animation: rack-in .5s cubic-bezier(.5, 0, .3, 1) both; }
  html:active-view-transition-type(back)::view-transition-old(rack) { animation-name: rack-out-back; }
  html:active-view-transition-type(back)::view-transition-new(rack) { animation-name: rack-in-back; }
  ::view-transition-group(aisle) { animation: none; }
}
@keyframes rack-out { to { transform: translateX(-70vw) scale(.94); opacity: 0; } }
@keyframes rack-in { from { transform: translateX(70vw) scale(.94); opacity: 0; } }
@keyframes rack-out-back { to { transform: translateX(70vw) scale(.94); opacity: 0; } }
@keyframes rack-in-back { from { transform: translateX(-70vw) scale(.94); opacity: 0; } }

/* ---------- the move without a reload (aisle.js) ----------
   The world moves by --hop-d (one rack and its aisle, 1028px; negative going back), the old rack fading into a
   neighbour, the hall moving less (parallax). Classes on <html>: hop (moving), hop-vt (with View Transitions,
   otherwise .hop-ghost is a copy of the old rack), hop-fwd / hop-back, hop-signs, hop-land. aisle.js sets the
   --hop-* variables and --cam0. */
:root {
  --hop-t: .72s;
  --hop-ease: cubic-bezier(.3, .75, .25, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
  :root { --hop-ease: linear(0, .024, .086, .17, .267, .368, .466, .559, .644, .719, .784, .839, .885, .922, .951, .973, .99, 1.002, 1.01, 1.015, 1.018, 1.018, 1.018, 1.016, 1.014, 1.012, 1.01, 1.007, 1.005, 1.003, 1.002, 1.001, 1); }
}
@media (prefers-reduced-motion: no-preference) {
  /* View Transitions: the old rack and its cables are snapshots, the rest of the page is live and new at once. */
  html.hop::view-transition { pointer-events: none; } /* a click in the middle of the move reaches the page */
  html.hop::view-transition-old(root), html.hop::view-transition-old(aisle) { animation: none; opacity: 0; }
  html.hop::view-transition-new(root), html.hop::view-transition-new(aisle) { animation: none; }
  html.hop::view-transition-group(rack), html.hop::view-transition-group(hop-cables),
  html.hop::view-transition-group(hop-power), html.hop::view-transition-group(hop-net) { animation: none; }
  html.hop::view-transition-old(rack) {
    transform-origin: 50% var(--hop-oy);
    animation: hop-out var(--hop-t) var(--hop-ease) both, hop-dim-out var(--hop-t) linear both;
  }
  html.hop::view-transition-new(rack) {
    transform-origin: 50% var(--hop-oy);
    animation: hop-in var(--hop-t) var(--hop-ease) both, hop-dim-in var(--hop-t) linear both;
  }
  html.hop::view-transition-old(hop-cables), html.hop::view-transition-old(hop-power), html.hop::view-transition-old(hop-net) {
    transform-origin: var(--hop-ox) var(--hop-ry);
    animation: hop-out var(--hop-t) var(--hop-ease) both, hop-dim-out var(--hop-t) linear both;
  }

  /* Without View Transitions: a copy of the old rack with its cables leaves, the new rack itself arrives. */
  html.hop:not(.hop-vt) .room { overflow: clip; }
  html.hop:not(.hop-vt) .room > .rack {
    transform-origin: 50% var(--hop-oy);
    animation: hop-in var(--hop-t) var(--hop-ease) both, hop-dim-in var(--hop-t) linear both;
  }
  .hop-ghost {
    position: absolute; inset: 0; z-index: 3; pointer-events: none;
    transform-origin: var(--hop-ox) var(--hop-ry);
    animation: hop-out var(--hop-t) var(--hop-ease) both, hop-dim-out var(--hop-t) linear both;
  }

  /* The new rack's cables show once it stands in place (app.js and power.js, the rack:landed event). */
  html.hop #cables, html.hop #power, html.hop #net { opacity: 0; }
  html.hop-land :is(#power, #net) { animation: hop-fade .45s ease-out both; }

  /* The hall: the neighbours move with the racks; the ceiling, the lamps and the floor from the last rack to this one. */
  html.hop .hall .nb { visibility: visible; animation: hop-nb var(--hop-t) var(--hop-ease) both; }
  html.hop .hall .nb.c { animation: hop-nb var(--hop-t) var(--hop-ease) both, hop-nb-out var(--hop-t) linear both; }
  html.hop-fwd .hall .nb.l, html.hop-back .hall .nb.r { animation: hop-nb var(--hop-t) var(--hop-ease) both, hop-nb-in var(--hop-t) linear both; }
  html.hop .hall .ceiling .plane { animation: hop-ceiling var(--hop-t) var(--hop-ease) both; }
  html.hop .hall .floor::before { animation: hop-floor var(--hop-t) var(--hop-ease) both; }
  html.hop .hall .lamp { animation: hop-lamp var(--hop-t) var(--hop-ease) both; }

  /* The signs swing on their chains, the new current one lights up like a fluorescent lamp, the old one goes dark. */
  html.hop-signs .aisle .signs a { transform-origin: 50% -60px; animation: sign-swing 1.15s ease-in-out both; }
  html.hop-signs .aisle .signs li:nth-child(2) a { animation-delay: .04s; }
  html.hop-signs .aisle .signs li:nth-child(3) a { animation-delay: .08s; }
  html.hop-signs .aisle .signs li:nth-child(4) a { animation-delay: .12s; }
  html.hop-signs .aisle .signs li:nth-child(n+5) a { animation-delay: .16s; }
  html.hop-signs .aisle .signs a[aria-current] { animation-name: sign-swing, sign-on; animation-duration: 1.15s, .7s; animation-timing-function: ease-in-out, linear; }
  html.hop-signs .aisle .signs a.was { animation-name: sign-swing, sign-off; animation-duration: 1.15s, .6s; animation-timing-function: ease-in-out, ease-out; }
  html.hop-signs .aisle .step > * { animation: step-in .5s cubic-bezier(.2, .8, .3, 1) both; }

  /* Waiting for the server: the sign we are going to blinks, the arrow nudges. */
  .aisle .signs a.pending { animation: sign-wait .5s steps(2, jump-none) infinite; }
  .aisle a.step.pending i { animation: step-nudge .45s ease-in-out infinite alternate; }
  .aisle a.step.prev.pending i { animation-name: step-nudge-back; }

  /* Arrived: the router brings the links up. */
  html.hop-land :is(.tor :is(.tp, .sfp-port).link, .rtr, .nic) .led { animation: link-up .5s steps(5, jump-none) both; }
}
html.hop-wait, html.hop-wait a { cursor: progress; }

@keyframes hop-out { to { transform: translateX(calc(var(--hop-d) * -1)) scale(.97); } }
@keyframes hop-in { from { transform: translateX(var(--hop-d)) scale(.97); } }
@keyframes hop-dim-out {
  35% { opacity: 1; filter: none; }
  to { opacity: 0; filter: brightness(var(--hop-dim, 1)) blur(var(--hop-blur, 0px)); }
}
@keyframes hop-dim-in {
  from { opacity: 0; filter: brightness(var(--hop-dim, 1)) blur(var(--hop-blur, 0px)); }
  55% { opacity: 1; filter: none; }
}
@keyframes hop-fade { from { opacity: 0; } }
@keyframes hop-nb { from { transform: translateX(var(--hop-d)); } }
@keyframes hop-nb-out { 30% { opacity: 1; } to { opacity: 0; } }
@keyframes hop-nb-in { from { opacity: 0; } 70% { opacity: 0; } }
@keyframes hop-ceiling { from { transform: rotateX(-66deg) translateX(calc((var(--cam0, 1) - 1) * -6vw)); } }
@keyframes hop-floor { from { transform: rotateX(64deg) translateX(calc((var(--cam0, 1) - 1) * -6vw)); } }
@keyframes hop-lamp { from { translate: calc((var(--cam0, 1) - 1) * -6vw) 0; } }
@keyframes sign-swing {
  0% { transform: none; }
  16% { transform: rotate(calc(var(--hop-s, 1) * -6deg)); }
  40% { transform: rotate(calc(var(--hop-s, 1) * 3.5deg)); }
  62% { transform: rotate(calc(var(--hop-s, 1) * -1.8deg)); }
  82% { transform: rotate(calc(var(--hop-s, 1) * .7deg)); }
  100% { transform: none; }
}
@keyframes sign-on {
  0% { filter: brightness(.35); }
  12% { filter: brightness(1.2); }
  20% { filter: brightness(.4); }
  32% { filter: brightness(1.1); }
  38% { filter: brightness(.6); }
  55%, 100% { filter: none; }
}
@keyframes sign-off { from { background: var(--sign-on); color: var(--sign-on-ink); box-shadow: 0 0 22px -2px color-mix(in srgb, var(--sign-on) 67%, transparent); } }
@keyframes sign-wait { 50% { box-shadow: inset 0 0 0 1px var(--accent), 0 0 16px -4px var(--accent); color: var(--ink); } }
@keyframes step-in { from { opacity: 0; transform: translateX(calc(var(--hop-s, 1) * 18px)); } }
@keyframes step-nudge { to { transform: translateX(4px); } }
@keyframes step-nudge-back { to { transform: translateX(-4px); } }
@keyframes link-up { 0% { opacity: .2; } 40% { opacity: 1; } 60% { opacity: .3; } 100% { opacity: 1; } }

/* rack.css */
/* The rack: the top, the rails with their holes (three per unit) and numbers, the units with ears and screws. */
.rack { position: relative; max-width: 1000px; margin: 0 auto; }
.cap {
  height: 26px; border-radius: 6px 6px 0 0;
  background: linear-gradient(var(--rail-hi), var(--rail));
  display: flex; align-items: center; justify-content: space-between; padding: 0 14px;
  font: 500 10px/1 var(--f-silk); letter-spacing: .16em; text-transform: uppercase; color: var(--num);
  box-shadow: inset 0 -1px 0 var(--edge);
}
.body {
  position: relative; padding: 0 var(--rail-w);
  background: linear-gradient(90deg, var(--void), color-mix(in srgb, var(--void) 70%, var(--rail)) 50%, var(--void));
}
.rail {
  position: absolute; top: 0; bottom: 0; width: var(--rail-w);
  background-color: var(--rail);
  box-shadow: inset 1px 0 0 var(--rail-hi), inset -1px 0 0 var(--edge);
}
.rail.l { left: 0; }
.rail.r { right: 0; }
.rail::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 8px;
  background: linear-gradient(to bottom, var(--hole) 0 8px, transparent 8px) 0 calc(var(--U) / 6 - 4px) / 8px calc(var(--U) / 3) repeat-y;
}
.rail.l::after { right: calc(var(--ear) / 2 - 4px); }
.rail.r::after { left: calc(var(--ear) / 2 - 4px); }
.rail .n {
  position: absolute; width: calc(var(--rail-w) - var(--ear)); text-align: center;
  font: 500 9px/1 var(--f-silk); color: var(--num); transform: translateY(-50%);
  font-variant-numeric: tabular-nums;
}
.rail.l .n { left: 0; }
.rail.r .n { right: 0; }
.foot {
  height: 30px; background: linear-gradient(var(--rail), var(--rail-hi));
  border-radius: 0 0 4px 4px; box-shadow: 0 18px 30px -12px #000a;
}

.u {
  --h: 1;
  position: relative; z-index: 1;
  display: grid; grid-template-columns: var(--ear) minmax(0, 1fr) var(--ear);
  min-height: calc(var(--U) * var(--h));
  margin-inline: calc(var(--ear) * -1);
}
.u::before, .u::after {
  content: ""; grid-row: 1;
  background:
    radial-gradient(circle 6px at 50% calc(var(--U) / 6), var(--screw-hi) 0 1.5px, var(--screw) 2px 4.2px, var(--edge) 4.6px 5.4px, transparent 5.8px),
    radial-gradient(circle 6px at 50% calc(100% - var(--U) / 6), var(--screw-hi) 0 1.5px, var(--screw) 2px 4.2px, var(--edge) 4.6px 5.4px, transparent 5.8px),
    var(--panel);
  box-shadow: inset 0 1px 0 var(--panel-hi), inset 0 -1px 0 var(--edge);
}
.u::before { grid-column: 1; border-radius: 3px 0 0 3px; }
.u::after { grid-column: 3; border-radius: 0 3px 3px 0; }
.face {
  grid-column: 2; grid-row: 1; min-width: 0;
  background: linear-gradient(var(--panel-hi), var(--panel) 18%, var(--panel));
  box-shadow: inset 0 1px 0 var(--panel-hi), inset 0 -1px 0 var(--edge);
  padding: 8px 14px;
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px;
}
.u.blank .face, .u.blank::before, .u.blank::after { background: transparent; box-shadow: none; }
.free .face { justify-content: center; }
.free .hand { font: 600 20px/1.1 var(--f-hand); color: var(--dim); }
@media (max-width: 560px) {
  .face { padding-inline: 10px; }
  .rail .n { font-size: 7.5px; }
}

/* parts.css */
/* Shared parts: silkscreen, label maker tapes, LEDs, LCDs, buttons, ports, stickers. */
.silk { font: 500 10px/1.2 var(--f-silk); letter-spacing: .16em; text-transform: uppercase; color: var(--dim); }
.tape {
  display: inline-block; padding: 3px 7px 2px; border-radius: 2px;
  background: var(--tape-k); color: var(--tape-ink);
  font: 700 11px/1.2 var(--f-silk); letter-spacing: .1em; text-transform: uppercase;
  text-shadow: 0 1px 0 #0008, 0 -1px 0 #ffffff1f;
  box-shadow: 0 1px 1px #0007;
  transform: rotate(-.7deg);
}
.tape.r { background: var(--tape-r); transform: rotate(.6deg); }
.tape.b { background: var(--tape-b); transform: rotate(-.3deg); }
.tapes { display: flex; flex-wrap: wrap; gap: 6px; }

.led { width: 8px; height: 8px; border-radius: 50%; flex: none; display: inline-block; background: var(--led-ok); box-shadow: 0 0 6px var(--led-ok), inset 0 -1px 1px #0005; }
.led.warn { background: var(--led-warn); box-shadow: 0 0 6px var(--led-warn); }
.led.err { background: var(--led-err); box-shadow: 0 0 6px var(--led-err); }
.led.pwr { background: var(--accent); box-shadow: 0 0 7px var(--accent); }
.led.off { background: var(--led-off); box-shadow: inset 0 1px 1px #0006; }
.leds { display: flex; gap: 12px; flex-wrap: wrap; }
.leds span { display: inline-flex; align-items: center; gap: 5px; }
/* Activity lamps (switch ports, network cards, the router, the server's net and hdd) blink while live.js sets
   .tx; otherwise the link lamp is steady and hdd is off. */
.led.hdd { background: var(--led-off); box-shadow: inset 0 1px 1px #0006; }
.led.tx { opacity: .15; }
.led.hdd.tx { opacity: 1; background: var(--led-ok); box-shadow: 0 0 6px var(--led-ok); }

.lcd {
  background: var(--lcd-bg); color: var(--lcd);
  font: 400 20px/1.05 var(--f-lcd); letter-spacing: .04em;
  padding: 7px 10px; border-radius: 3px;
  box-shadow: inset 0 0 0 2px var(--edge), inset 0 3px 10px #0007;
  text-shadow: 0 0 6px color-mix(in srgb, var(--lcd) 45%, transparent);
  min-width: 0;
}
.lcd .d { color: var(--lcd-dim); }
.lcd a { text-decoration: none; }
.lcd a:hover { text-decoration: underline; }
/* Messages that take turns (cycle, app.js) are stacked: the screen is as large as the longest one, so it never
   jumps. */
.lcd .msgs { list-style: none; margin: 0; padding: 0; display: grid; }
.lcd .msgs > li { grid-area: 1 / 1; }
.lcd .msgs li:not(.on) { visibility: hidden; }

.key {
  font: 600 12px/1 var(--f-silk); letter-spacing: .08em; text-transform: uppercase;
  color: var(--key-ink); background: var(--key); text-decoration: none;
  border: 1px solid var(--edge); border-radius: 4px; padding: 8px 11px; min-height: 30px;
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  box-shadow: inset 0 1px 0 var(--panel-hi), 0 2px 0 var(--edge);
}
.key:active { transform: translateY(1px); box-shadow: inset 0 1px 0 var(--panel-hi), 0 1px 0 var(--edge); }
.key:disabled { opacity: .5; cursor: default; }
.key:disabled:active { transform: none; }

.ports { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; }
.port { display: inline-flex; align-items: center; gap: 8px; min-height: 30px; text-decoration: none; }
.jack {
  width: 22px; height: 18px; flex: none; position: relative;
  background: var(--hole); border-radius: 2px;
  clip-path: polygon(0 0, 100% 0, 100% 72%, 76% 72%, 76% 100%, 24% 100%, 24% 72%, 0 72%);
  box-shadow: inset 0 0 0 2px #000;
}
.jack::after { content: ""; position: absolute; left: 4px; right: 4px; top: 3px; height: 2px; background: repeating-linear-gradient(90deg, #c9a640 0 1px, transparent 1px 3px); }
.port:hover .tape { filter: brightness(1.25); }
.port .led { width: 6px; height: 6px; }
.port.off .tape { opacity: .55; }
/* A port that copies a name (switch.html): its tape is as wide as its label, like the others'. A click sticks a tape
   with the name (and ✓ once copied) over it, wider, out of the flow, so nothing moves. */
button.port { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.port.shown { position: relative; z-index: 1; }
.tape.swap { position: relative; }
.tape.swap > .alt {
  position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%) rotate(1deg);
  padding: inherit; border-radius: inherit; background: inherit; white-space: nowrap;
  box-shadow: 0 2px 4px #0009;
}
.tape.swap > .alt, .tape.swap .alt > i { visibility: hidden; }
.port.shown .tape.swap > .alt, .port.copied .tape.swap .alt > i { visibility: visible; }
.tape.swap .alt > i { font-style: normal; }

/* Icons (templates/icons.html): lines in the text's colour, 1em. */
.ic { width: 1em; height: 1em; flex: none; vertical-align: -.14em; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.ic .solid { fill: currentColor; stroke: none; }

/* units/network.css */
/* The network bay (patch.html, network.html): the patch panel, the brush organiser, the switch, the router,
   and the units' network cards and the vertical organiser that app.js adds. */

/* ---------- patch panel ---------- */
.patch .face { gap: 14px; }
.pp-id { display: grid; gap: 3px; flex: none; }
.brand { font: 700 12px/1 var(--f-silk); letter-spacing: .1em; color: var(--ink); white-space: nowrap; }
.pp-ports { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; counter-reset: port; flex: 1 1 auto; min-width: 0; }
.pp-ports .port { position: relative; padding-top: 9px; }
.pp-ports .pn { position: absolute; top: -1px; left: 0; width: 22px; text-align: center; font: 500 8px/1 var(--f-silk); color: var(--num); }
.pp-ports .pn::before { counter-increment: port; content: counter(port); }
/* The neighbours' names are the ring's: a long one is cut with an ellipsis (the hint has it whole) so the row holds. */
.pp-ports .ring .nm { display: inline-block; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: top; }
/* The free jacks take what is left of the row, a jack every 28 px as on a real panel, and only as many as fit: the
   rest land in rows of no height, out of sight, so a long name never pushes jacks to a second row (app.js hangs the
   guests' cords on the visible ones). Narrower than a jack, the strip shows none; on a phone there is none. */
.pp-free {
  display: grid; grid-template-columns: repeat(auto-fill, 28px); grid-template-rows: auto; grid-auto-rows: 0;
  justify-items: start; overflow: hidden; padding-top: 1px; margin-top: -1px;
  flex: 1 1 0; min-width: 0; container-type: inline-size;
}
.pp-free .port { gap: 0; }
/* A container keeps the panel's counter out (Chrome): the free jacks count on from the ring's four by themselves. */
.pp-free .port:first-child { counter-reset: port 4; }
@container (width < 22px) { .pp-free .port { display: none; } }
@media (max-width: 860px) { .pp-free { display: none; } }

/* ---------- brush organiser ---------- */
.brush .face { gap: 14px; padding-block: 6px; }
.brush .slot {
  flex: 1; height: 18px; border-radius: 9px; background: var(--hole); position: relative; overflow: hidden;
  box-shadow: inset 0 0 0 2px #000, 0 1px 0 var(--panel-hi);
}
.brush .slot::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(90deg, #2a2d32 0 1px, transparent 1px 3px); }
.brush .slot::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: #000; }
/* A phone: no brushes (the cords go straight), the patch panel's rows closer. */
@media (max-width: 560px) {
  .u.brush { display: none; }
  .patch .face { row-gap: 8px; }
}

/* ---------- switch ---------- */
.tor .face, .rtr .face { gap: 16px; }
.tor-id { display: grid; gap: 3px; flex: none; }
.tor-ports { display: grid; grid-auto-flow: column; grid-template-rows: repeat(2, auto); grid-auto-columns: 22px; gap: 3px 5px; }
.tp { display: grid; justify-items: center; gap: 1px; }
.tp:nth-child(even) { grid-template-rows: auto auto; }
.tp:nth-child(even) .led { order: 2; }
.tp .led, .sfp-port .led { width: 4px; height: 4px; background: var(--led-off); box-shadow: none; }
.tp.link .led, .sfp-port.link .led { background: var(--led-ok); box-shadow: 0 0 5px var(--led-ok); }
.tp .jack { width: 20px; height: 15px; }
.tp .jack::after { left: 3px; right: 3px; top: 2px; }
.tor-sfp { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.sfp-port { display: grid; justify-items: center; gap: 1px; }
.sfp { width: 26px; height: 14px; border-radius: 1px; background: var(--hole); box-shadow: inset 0 0 0 2px #2a2d32; flex: none; }
.tor .leds, .rtr .leds { display: grid; gap: 4px; }
.rtr .leds { grid-auto-flow: column; column-gap: 10px; } /* pwr, wan, lan: a row, so the router stays 1U */
@media (max-width: 760px) { .tp:nth-child(n + 9) { display: none; } .tor-sfp { display: none; } }

/* ---------- router ---------- */
.rtr-ports { display: flex; align-items: center; gap: 6px; }
/* The WAN jack is framed in blue, as on a router; a ring outside the jack would be cut by its clip-path. */
.rtr-ports .jack.wan { box-shadow: inset 0 0 0 2px var(--tape-b); margin-right: 8px; }
.rtr .lcd { margin-left: auto; font-size: 17px; padding: 4px 10px; white-space: nowrap; }
/* The site's other ways in: link jacks after LAN; a lamp is a Gatus check, dark without the router's WAN or LAN. */
.rtr .uplinks { display: flex; align-items: center; gap: 10px; margin-left: 6px; padding-left: 12px; border-left: 1px solid var(--edge); }
.rtr .port.up { gap: 5px; min-height: 26px; }
.rtr .port.up .tape { padding: 2px 5px 1px; font-size: 10px; }
.rtr .port.up .led.off, .rtr:is(.nowan, .nolan) .port.up .led { background: var(--led-off); box-shadow: none; }
@media (max-width: 760px) { .rtr .lcd { display: none; } .rtr .leds { margin-left: auto; } }

/* ---------- network cards and the vertical organiser (app.js) ---------- */
.nic {
  position: absolute; z-index: 2; left: calc(var(--ear) + 4px); top: 50%; transform: translateY(-50%);
  display: grid; justify-items: center; gap: 2px;
}
.nic .jack { width: 16px; height: 13px; }
.nic .jack::after { left: 3px; right: 3px; top: 2px; }
.nic .led { width: 4px; height: 4px; }
/* A guest (data-host) has no card but keeps its host's inset. */
.u:is(.has-nic, [data-host]) .face { padding-left: 38px; }
/* Without power or a link (.unlinked, net.js) the card's lamp is dark. */
.u.off .nic .led, .u.unlinked .nic .led { background: var(--led-off); box-shadow: none; animation: none; }
/* A cord just plugged back in (.linking, net.js): its lamps blink amber while the link comes up. */
.tp.linking .led, .sfp-port.linking .led, .u.linking .nic .led { background: var(--led-warn); box-shadow: 0 0 5px var(--led-warn); }
@media (prefers-reduced-motion: no-preference) {
  .tp.linking .led, .sfp-port.linking .led, .u.linking .nic .led { animation: linking .5s steps(2, jump-none) infinite; }
}
@keyframes linking { 50% { opacity: .2; } }
.vm {
  position: absolute; z-index: 0; top: 0; bottom: 0; left: -22px; width: 20px; border-radius: 3px;
  background: repeating-linear-gradient(#1a1c20 0 9px, #0d0e10 9px 22px);
  box-shadow: inset 0 0 0 1px #000, inset 3px 0 0 #222529;
}
@media (max-width: 760px) { .vm, .nic { display: none; } .u:is(.has-nic, [data-host]) .face { padding-left: 14px; } }
/* Phone: the switch and the router in one unit each, without lamps and spare captions. */
@media (max-width: 560px) {
  .tor .leds, .rtr .leds, .rtr-ports .silk, .tor-id .silk { display: none; }
  .tor .face, .rtr .face { gap: 10px; }
  /* The ways in without jacks, a tape with a lamp: otherwise the router does not fit in one unit. */
  .rtr .uplinks { gap: 7px; margin-left: 0; padding-left: 8px; }
  .rtr .port.up .jack { display: none; }
  .rtr .port.up { gap: 4px; }
}

/* ---------- offline ----------
   net.js marks every unit on the network: .unlinked (no link: "link lost", "no internet") or .nonet (a link
   but no internet). The data stays in the markup, hidden; without power a unit says "no signal" (power.css), booting
   it shows its boot first (toys.css). */
.u:is(.unlinked, .nonet):not(.off, .posting) :is(.lcd, .screen)[data-nolink] { position: relative; }
.u:is(.unlinked, .nonet):not(.off, .posting) :is(.lcd, .screen)[data-nolink] * { visibility: hidden; }
.u:is(.unlinked, .nonet):not(.off, .posting) .lcd[data-nolink] { color: transparent; text-shadow: none; }
/* The notice over the screen, not in the flow: otherwise the screen grows, the units below move and the cables stay. */
.u:is(.unlinked, .nonet):not(.off, .posting) :is(.lcd, .screen)[data-nolink]::after {
  content: attr(data-nonet); position: absolute; inset: 0; display: grid; place-content: center; text-align: center;
  white-space: pre-line; font: 400 20px/1.05 var(--f-lcd); letter-spacing: .06em;
  color: var(--lcd); text-shadow: 0 0 6px color-mix(in srgb, var(--lcd) 45%, transparent);
}
/* Without a link: two lines; a unit on the rack's network only (no data-nonet): one. */
.u.unlinked:not(.off, .posting) :is(.lcd, .screen)[data-nolink]::after { content: attr(data-nolink) "\A" attr(data-nonet); }
.u.unlinked:not(.off, .posting) :is(.lcd, .screen)[data-nolink]:not([data-nonet])::after { content: attr(data-nolink); }
.u:is(.unlinked, .nonet):not(.off, .posting) .screen[data-nolink]::after { color: var(--crt-dim); text-shadow: none; }
@media (prefers-reduced-motion: no-preference) {
  .u:is(.unlinked, .nonet):not(.off, .posting) :is(.lcd, .screen)[data-nolink]::after { animation: nolink 1.6s steps(2, jump-none) infinite; }
}
@keyframes nolink { 50% { opacity: .35; } }
/* Live data (data-live) goes dark offline: lamps and bars off, texts gone, the picture's tray empty. */
.u:is(.unlinked, .nonet):not(.off, .posting) :is([data-live~="lamps"], [data-live~="bar"]) i { background: var(--led-off); box-shadow: none; animation: none; }
.u:is(.unlinked, .nonet):not(.off, .posting) :is([data-live~="text"], [data-live~="bar"] b) { color: transparent; text-shadow: none; }
.u:is(.unlinked, .nonet):not(.off, .posting) [data-live~="image"] {
  object-position: 200px 0; background: repeating-linear-gradient(135deg, var(--void) 0 6px, var(--panel) 6px 12px);
}
/* The link lamps (the network card's and the unit's own net lamp) say only whether there is a link. */
.u.unlinked .led.net { background: var(--led-off); box-shadow: none; animation: none; }

/* The drawer of an offline unit shows only the error (data-* on .slide); one pulled out offline is loaded
   once the unit is back (data-later, net.js). */
.u:is(.unlinked, .nonet):not(.off) + .drawer.open .slide { position: relative; }
.u:is(.unlinked, .nonet):not(.off) + .drawer.open .slide > * { visibility: hidden; }
.u:is(.unlinked, .nonet):not(.off) + .drawer.open .slide::after {
  content: attr(data-nonet); position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  white-space: pre; text-align: center; padding: 8px 18px; border-radius: 3px;
  font: 400 20px/1.05 var(--f-lcd); letter-spacing: .06em;
  background: var(--lcd-bg); color: var(--lcd); text-shadow: 0 0 6px color-mix(in srgb, var(--lcd) 45%, transparent);
  box-shadow: inset 0 0 0 2px var(--edge), inset 0 3px 10px #0007;
}
.u.unlinked:not(.off) + .drawer.open .slide::after { content: attr(data-nolink) "\A" attr(data-nonet); }
@media (prefers-reduced-motion: no-preference) {
  .u:is(.unlinked, .nonet):not(.off) + .drawer.open .slide::after { animation: nolink 1.6s steps(2, jump-none) infinite; }
}
.drawer[data-later] .slide { min-height: calc(var(--U) * 2); }

/* The router without WAN or LAN (net.js): "no link" on its LCD, the WAN lamp dark. */
.rtr:is(.nowan, .nolan):not(.off, .posting) .lcd { position: relative; color: transparent; text-shadow: none; }
.rtr:is(.nowan, .nolan):not(.off, .posting) .lcd::after {
  content: attr(data-nolan); position: absolute; inset: 0; display: grid; place-content: center; font: inherit;
  white-space: nowrap; color: var(--lcd); text-shadow: 0 0 6px color-mix(in srgb, var(--lcd) 45%, transparent);
}
.rtr.nowan:not(.off, .posting) .lcd::after { content: attr(data-nowan); }
.rtr.nowan .led.wan, .rtr.nolan .led.lan { background: var(--led-off); box-shadow: none; animation: none; }
@media (prefers-reduced-motion: no-preference) {
  .rtr:is(.nowan, .nolan):not(.off, .posting) .lcd::after { animation: nolink 1.6s steps(2, jump-none) infinite; }
}

/* units/patch.css */
/* The patch panel is the otoring webring's widget. */
.patch .face { justify-content: space-between; }

/* units/mgmt.css */
/* Management: sections on the left, toggles on the right. */
.mgmt .face { justify-content: space-between; }
.mgmt .ports { gap: 6px; }
/* Language: two keys, the pressed one sunk and lit. */
.langs { display: inline-flex; gap: 0; }
.langs .key { border-radius: 0; padding-inline: 9px; }
.langs .key:first-child { border-radius: 4px 0 0 4px; }
.langs .key:last-child { border-radius: 0 4px 4px 0; border-left: 0; }
.langs .key.on {
  color: var(--accent); transform: translateY(1px);
  box-shadow: inset 0 2px 3px #0006, 0 1px 0 var(--edge);
}
/* Sound (assets/js/sound.js): only an icon, a speaker or a crossed-out one; the caption in title and aria-label. */
.mgmt .snd { padding: 0 10px; min-height: 30px; font-size: 14px; }
.mgmt .snd[aria-pressed="true"] .i-off, .mgmt .snd[aria-pressed="false"] .i-on { display: none; }

/* units/server.css */
/* The server with my name: a photo, a nameplate, tapes with facts, an LCD with lines taking turns. */
.srv .face { align-items: stretch; gap: 14px 20px; padding-block: 14px; }
.photo {
  width: 112px; align-self: flex-start; aspect-ratio: 1; max-width: 100%; flex: none; border-radius: 4px; position: relative; overflow: hidden;
  background:
    radial-gradient(circle at 50% 38%, #c9a8f5 0 21%, transparent 22%),
    radial-gradient(ellipse 60% 40% at 50% 100%, #8a5fd0 0 98%, transparent 100%),
    linear-gradient(160deg, #3c2f63, #1c2747);
  box-shadow: inset 0 0 0 3px var(--edge);
}
.photo::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none; background: repeating-linear-gradient(0deg, #0000 0 2px, #0003 2px 3px); }
.photo span { position: absolute; left: 6px; bottom: 5px; z-index: 1; font: 500 9px/1 var(--f-silk); letter-spacing: .14em; color: #fffc; }
.ident { flex: 1 1 320px; min-width: 0; display: grid; gap: 8px; align-content: start; }
.name { font: 700 clamp(28px, 5.4vw, 48px)/0.95 var(--f-silk); letter-spacing: .02em; margin: 0; text-wrap: balance; }
.name small { display: block; font: 500 11px/1.4 var(--f-silk); letter-spacing: .18em; color: var(--dim); text-transform: uppercase; margin-top: 6px; }
.intro { margin: 0; max-width: 60ch; color: var(--ink); }
.status-col { flex: 0 1 230px; display: grid; gap: 8px; align-content: start; min-width: 0; }
/* Narrower than 940px (the album, the name's column and this one no longer share the row: the face loses 174px to
   the ears, its padding and the card and supply insets) the status column wraps under the intro: it takes the whole
   row then, the screen and the lamps first and the stickers side by side, as many as fit, instead of a 230px strip
   with a blank right side. */
@media (max-width: 939px) and (min-width: 561px) {
  .status-col { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px 14px; }
  .status-col > .lcd, .status-col > .leds { flex: 1 1 100%; }
  .status-col .lcd.two { max-width: 420px; }
}
.lcd.two { font-size: 22px; }
/* Avatars from Telegram: a square that swipes (scroll-snap); with JS a remote under it with keys, a counter
   and a lamp per photo. */
.album { width: 176px; flex: none; align-self: flex-start; display: grid; gap: 8px; }
.photo.gallery { width: 100%; height: auto; background: var(--void); }
.gallery .slides {
  display: flex; width: 100%; height: 100%; overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scrollbar-width: none;
}
.gallery .slides::-webkit-scrollbar { display: none; }
.gallery .slides:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* One photo a swipe: without snap-stop a quick flick flies past several before it snaps. */
.gallery img { flex: 0 0 100%; width: 100%; height: 100%; object-fit: cover; scroll-snap-align: center; scroll-snap-stop: always; }
.gal-ctl { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 6px; }
.gal-ctl .key { padding: 0; width: 34px; min-height: 28px; justify-content: center; font-size: 15px; }
.gal-ctl .lcd { font-size: 18px; padding: 4px 6px; text-align: center; white-space: nowrap; }
.gal-leds { grid-column: 1 / -1; display: flex; justify-content: center; gap: 7px; padding-top: 1px; }
.gal-leds .led { width: 6px; height: 6px; }
@media (prefers-reduced-motion: no-preference) { .gal-leds .led { transition: background-color .2s, box-shadow .2s; } }
/* A phone: the name and the words about me first, the photos and the LCD under them. */
@media (max-width: 560px) {
  .ident { order: -1; }
  .photo { width: 84px; }
  .album { width: 128px; }
  .gal-leds { gap: 5px; }
}

/* Favourite colours on the LCD: squares framed in the screen's colour. */
.lcd .swatches { display: inline-flex; gap: 6px; vertical-align: middle; padding-top: 3px; }
.lcd .swatches i { width: 18px; height: 18px; border-radius: 2px; background: var(--c); box-shadow: 0 0 0 1.5px var(--lcd), 0 0 6px color-mix(in srgb, var(--lcd) 40%, transparent); }
/* The asset tag sticker: a white label with a barcode, the PGP key's fingerprint printed on it. */
.asset-tag {
  display: grid; gap: 3px; justify-self: start; max-width: 100%; padding: 5px 8px 6px; border-radius: 2px;
  background: #f2efe4; color: #15161a; box-shadow: inset 0 0 0 1px #0002, 0 1px 0 #0005; transform: rotate(-.4deg);
}
.asset-tag .silk { color: #4b4d52; font-size: 8.5px; }
.asset-tag code { font: 500 11px/1.3 ui-monospace, "Cascadia Mono", Consolas, monospace; letter-spacing: .02em; max-width: 25ch; user-select: all; }
.asset-tag.age { margin-top: 4px; transform: rotate(.5deg); }
.asset-tag.age code { word-break: break-all; }
.asset-tag.ssh { margin-top: 4px; transform: rotate(-.3deg); }
.asset-tag.ssh code { max-width: 100%; overflow-wrap: anywhere; }
.asset-tag a.silk { text-decoration: underline dotted; text-underline-offset: 2px; }
.asset-tag .bars { height: 12px; background: repeating-linear-gradient(90deg, #15161a 0 1px, transparent 1px 3px, #15161a 3px 5px, transparent 5px 6px, #15161a 6px 7px, transparent 7px 9px); }

/* units/media.css */
/* The media server is Koito: the cover in the tray, the track on the LCD, the VU meter. */
.media .face { gap: 12px 18px; }
.tray {
  width: 84px; height: 84px; max-width: 100%; flex: none; display: block; object-fit: cover;
  border-radius: 3px; border: 3px solid var(--edge); background: var(--void);
  box-shadow: 0 0 0 1px var(--panel-hi);
}
.tray.empty { background: repeating-linear-gradient(135deg, var(--void) 0 6px, var(--panel) 6px 12px); }
.media .lcd { flex: 1 1 300px; font-size: 24px; }
.media .lcd .row2 { font-size: 18px; }
.vu { display: grid; gap: 3px; flex: 0 1 200px; min-width: 150px; }
.vu .row { display: grid; grid-template-columns: repeat(14, 1fr); gap: 2px; min-height: 9px; }
.vu i { height: 9px; border-radius: 1px; background: var(--led-off); }
.vu i.on { background: var(--led-ok); box-shadow: 0 0 4px var(--led-ok); }
.vu i.on.w { background: var(--led-warn); box-shadow: 0 0 4px var(--led-warn); }
.vu i.on.e { background: var(--led-err); box-shadow: 0 0 4px var(--led-err); }

/* units/monitor.css */
/* Monitoring is Gatus: a lamp per check and an LCD with the summary. */
.mon .face { align-items: flex-start; gap: 12px 20px; padding-block: 12px; }
.matrix { flex: 1 1 420px; display: grid; grid-template-columns: repeat(var(--cols, 9), minmax(0, 1fr)); gap: 8px 6px; min-width: 0; }
.cell { display: grid; justify-items: center; gap: 5px; text-align: center; }
.cell .silk { font-size: 8.5px; letter-spacing: .08em; overflow-wrap: anywhere; }
.mon .lcd { flex: 0 1 210px; font-size: 20px; }
@media (max-width: 760px) {
  .matrix { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* units/syslog.css */
/* The log: listens from Koito and rare events, scrolling inside the screen. Events dimmer than the music. */
.log .face { align-items: stretch; flex-direction: column; padding-block: 12px; }
.log .face > * { flex-shrink: 0; }
.log .bar { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; }
.log .lcd { font-size: 19px; line-height: 1.25; padding-right: 6px; }
.log ol {
  list-style: none; margin: 0; padding: 0 6px 0 0; display: grid; gap: 2px; align-content: start;
  height: calc(var(--U) * 3.4); overflow: auto; overscroll-behavior: contain;
  scrollbar-color: var(--lcd-dim) transparent; scrollbar-width: thin;
}
.log ol:focus-visible { outline: 1px dashed var(--lcd-dim); outline-offset: 2px; }
.log li { display: grid; grid-template-columns: 10ch 5ch minmax(0, 1fr); gap: 10px; white-space: nowrap; }
.log li span:last-child { overflow: hidden; text-overflow: ellipsis; }
.log li.ev { color: var(--lcd-dim); }
.log li.ev a { color: inherit; }
.log li.warn { color: var(--led-warn); }
.log li.err { color: var(--led-err); }
@media (max-width: 560px) {
  .log li { grid-template-columns: 10ch minmax(0, 1fr); }
  .log li .src { display: none; }
  .log ol { height: calc(var(--U) * 4); }
}

/* units/tokens.css */
/* The token meter: three counts on an LCD, the bill in silkscreen beside them; stale when the machines are quiet. */
.tokens .face { gap: 8px 18px; padding-block: 10px; }
.tok-lcd { display: flex; flex-wrap: wrap; gap: 2px 18px; font-size: 19px; }
.tok-lcd > span { white-space: nowrap; }
.tok-lcd.stale { color: var(--lcd-dim); }
.tok-bill { white-space: nowrap; }

/* units/activity.css */
/* The activity unit: the name and the numbers in a row, under them an LCD with a cell a day, seven to a column, in
   the cover's colours like every screen. Where the row is narrow the oldest weeks go out of view inside the screen's
   frame. Booting, the cells blink: a lamp test. */
.act .face { flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 8px; padding-block: 12px; }
.act-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px; }
.act-head .act-meta { margin-left: auto; }
.act-wall { padding: 9px 10px; }
.act-view { display: flex; justify-content: flex-end; overflow: hidden; } /* the grid centred while it fits (its auto margins), the newest weeks kept when it does not */
.act-grid { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 10px); grid-auto-columns: 10px; gap: 3px; flex: none; margin-inline: auto; }
.act-grid b { display: block; border-radius: 2px; background: color-mix(in srgb, var(--lcd) 12%, var(--lcd-bg)); }
.act-grid b.n { background: transparent; }
.act-grid b.l1 { background: color-mix(in srgb, var(--lcd) 35%, var(--lcd-bg)); }
.act-grid b.l2 { background: color-mix(in srgb, var(--lcd) 58%, var(--lcd-bg)); }
.act-grid b.l3 { background: color-mix(in srgb, var(--lcd) 80%, var(--lcd-bg)); }
.act-grid b.l4 { background: var(--lcd); box-shadow: 0 0 5px color-mix(in srgb, var(--lcd) 60%, transparent); }
@media (prefers-reduced-motion: no-preference) { .act.posting .act-grid b { animation: act-post .5s steps(1) infinite; } }
@keyframes act-post { 50% { background: var(--lcd); } }
/* No internet, or the cord out: the cells go out from the newest week back, under the screen's "no internet" notice
   (network.css), and come back the same way. The notice hides the screen's children; the cells stay for the wipe. */
.act-grid { clip-path: inset(0 0 0 0); transition: clip-path 1.4s steps(53); }
.act:is(.unlinked, .nonet) .act-grid { clip-path: inset(0 100% 0 0); }
.u.act:is(.unlinked, .nonet):not(.off, .posting) .lcd[data-nolink] * { visibility: visible; }

/* units/kvm.css */
/* The KVM console is the guestbook: a screen with the entries and a keyboard with the form and the canvas. */
.kvm .face { flex-direction: column; align-items: stretch; padding-block: 12px; gap: 10px; }
/* The parts do not shrink: with the canvas open the unit should grow, not the form flatten. */
.kvm .face > * { flex-shrink: 0; }
.kvm .bar { display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.screen {
  background: var(--crt); border-radius: 4px; padding: 10px 12px;
  box-shadow: inset 0 0 0 3px var(--edge), inset 0 0 30px var(--crt-shade);
  color: var(--crt-ink); display: grid; gap: 8px; font-size: 14px;
  max-height: 380px; overflow-y: auto; scrollbar-color: var(--crt-faint) transparent;
}
.screen .e { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; border-bottom: 1px dashed var(--crt-rule); padding-bottom: 8px; }
.screen .e:last-child { border-bottom: 0; padding-bottom: 0; }
.screen .e > :not(.pic) { grid-column: 1; }
.screen .m { font: 500 10px/1.3 var(--f-silk); letter-spacing: .1em; text-transform: uppercase; color: var(--crt-dim); }
.screen .txt { margin: 0; white-space: pre-line; overflow-wrap: anywhere; }
.screen .rep { margin: 0; color: var(--crt-dim); white-space: pre-line; overflow-wrap: anywhere; }
/* The reply's corner is drawn with borders: many fonts have no ↳. */
.screen .rep::before { content: ""; display: inline-block; width: .55em; height: .55em; margin: 0 .45em 0 .1em; border-left: 1.5px solid; border-bottom: 1.5px solid; vertical-align: .2em; }
.screen .empty { margin: 0; color: var(--crt-dim); }
.screen .gb-pages { display: flex; gap: 12px; font: 500 10px/1.3 var(--f-silk); letter-spacing: .1em; text-transform: uppercase; }
.screen .gb-pages a { color: var(--crt-dim); padding: 6px 0; }
.screen .gb-pages .older { margin-left: auto; }
.screen .pic { grid-column: 2; grid-row: 1 / span 3; line-height: 0; }
.screen .pic img { width: 120px; height: auto; aspect-ratio: 16 / 10; object-fit: contain; background: #fff; border: 1px solid var(--crt-rule); border-radius: 2px; }

.screen .pic { cursor: zoom-in; }

/* A drawing large over the page; without JS the link just opens the picture. */
.lightbox {
  padding: 10px; border: 0; border-radius: 6px; max-width: min(92vw, 980px);
  background: var(--panel); color: var(--ink); box-shadow: 0 0 0 1px var(--edge), 0 30px 80px -20px #000;
}
.lightbox::backdrop { background: #000c; }
.lightbox img { display: block; width: min(88vw, 960px); height: auto; aspect-ratio: 16 / 10; object-fit: contain; background: #fff; border-radius: 3px; }
.lightbox .lb-bar { display: flex; gap: 6px; align-items: center; justify-content: center; margin-top: 8px; }

.gb-status { margin: 0; font-size: 14px; padding: 6px 10px; border-radius: 4px; border: 1px solid var(--edge); }
.gb-status.ok { color: var(--led-ok); }
.gb-status.err { color: var(--led-err); }

.kb { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 10px; align-items: start; position: relative; }
.kb .fields { display: grid; gap: 6px; min-width: 0; }
.kb input[type="text"], .kb input:not([type]), .kb textarea {
  width: 100%; background: var(--panel-hi); color: var(--ink); border: 1px solid var(--edge); border-radius: 4px;
  font: 400 14px/1.4 var(--f-text); padding: 7px 10px; box-shadow: inset 0 2px 4px #0003;
}
.kb textarea { min-height: 70px; resize: vertical; }
.kb .keys { display: grid; gap: 6px; align-content: start; }
.kb .opts { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; color: var(--dim); align-items: center; justify-content: space-between; }
.kb label { display: inline-flex; align-items: center; gap: 6px; min-height: 26px; }
.kb .hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* The drawing pad (assets/js/oekaki.js). flex, not grid: in a grid the row was sized by the canvas's natural size. */
.pad { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: stretch; gap: 10px; min-width: 0; }
.pad .stage { display: flex; justify-content: center; min-width: 0; }
.pad canvas {
  width: 640px; max-width: 100%; aspect-ratio: 16 / 10; height: auto; background: #fff;
  border: 3px solid var(--edge); border-radius: 3px; touch-action: none; cursor: crosshair;
  box-shadow: 0 10px 22px -14px #000;
}
.pad .tools { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; justify-content: center; }
.pad .grp { display: inline-flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.pad .key { min-height: 30px; min-width: 30px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.pad .key[aria-pressed="true"] { color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.pad .key:disabled { opacity: .4; cursor: default; }
.pad .swatches { max-width: 360px; justify-content: center; }
.pad .sw { width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--panel); box-shadow: 0 0 0 1px var(--edge); cursor: pointer; padding: 0; }
.pad .sw[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--ink); }
.pad .sw.custom { position: relative; overflow: hidden; background: conic-gradient(#e0322b, #ffd23f, #3daa63, #2bb3c0, #3a7bd5, #7b4fd6, #e0322b); }
.pad .sw.custom input { position: absolute; inset: -6px; width: 40px; height: 40px; opacity: 0; cursor: pointer; }
.pad .size { gap: 8px; }
.pad .size input { width: 110px; accent-color: var(--accent); }
.pad .size .dot { flex: none; border-radius: 50%; max-width: 40px; max-height: 40px; box-shadow: 0 0 0 1px var(--edge); }
.pad .pad-warn { flex-basis: 100%; margin: 0; text-align: center; font-size: 13px; color: var(--led-warn); }

/* Full screen: the canvas as large as fits, the tools under it. The page does not scroll while it is open. */
.pad.full {
  position: fixed; inset: 0; z-index: 60; background: var(--panel); justify-content: center;
  padding: max(12px, env(safe-area-inset-top)) 12px max(12px, env(safe-area-inset-bottom));
}
.pad.full .stage { flex: 1 1 auto; min-height: 0; align-items: center; }
.pad.full canvas { width: auto; height: auto; max-width: 100%; max-height: 100%; }
:root.pad-full { overflow: hidden; }
/* Full screen: raise the rack and the unit over the aisle and the cables. */
:root.pad-full .room > .rack { z-index: 70; }
:root.pad-full .u.kvm { z-index: 80; }
/* A phone on its side: the tools right of the canvas. */
@media (orientation: landscape) and (max-height: 520px) {
  .pad.full { flex-direction: row; align-items: center; }
  .pad.full .tools { flex: 0 0 auto; width: min(300px, 42vw); max-height: 100%; overflow-y: auto; }
}

@media (max-width: 560px) {
  .kb { grid-template-columns: 1fr; }
  .kb .keys { grid-auto-flow: column; justify-content: start; }
  .screen .pic img { width: 88px; }
  .pad .key span { display: none; }
  .pad .size input { width: 90px; }
}

/* units/stickers.css */
/* 88x31 buttons: two blank panels, each with a captioned tape and a wall of buttons. */
.stick .face { flex-direction: column; align-items: flex-start; gap: 8px; padding-block: 10px; }
.wall { display: flex; flex-wrap: wrap; gap: 4px; }
.b88 {
  display: block; width: 88px; height: 31px; flex: none; line-height: 0;
  box-shadow: 0 1px 2px #0008; transition: transform .12s;
}
.b88 img { width: 88px; height: 31px; image-rendering: pixelated; }
a.b88:hover, a.b88:focus-visible { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .b88 { transition: none; } }

/* A narrow wall shows three rows (floor((width + 4) / 92) columns), the rest behind "show all": a <details>
   without content, read by :has([open]); without :has() everything shows. app.js re-lays the rack when it opens. */
.wall { container: wall / inline-size; width: 100%; }
.more { display: none; flex: 1 0 100%; margin-top: 4px; }
.more > summary { list-style: none; width: max-content; }
.more > summary::-webkit-details-marker { display: none; }
.more > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.more[open] .m-all, .more:not([open]) .m-less { display: none; }
.more[open] .ic { transform: rotate(180deg); }
@container wall (width < 548px) { .more { display: block; } }
@container wall (width < 272px) {
  .wall:has(> .more:not([open])) > .b88:nth-child(n+7) { display: none; }
  .wall:has(> .more[open]) > .b88:nth-child(n+7) { animation: b88-in .25s ease-out backwards; }
}
@container wall (272px <= width < 364px) {
  .wall:has(> .more:not([open])) > .b88:nth-child(n+10) { display: none; }
  .wall:has(> .more[open]) > .b88:nth-child(n+10) { animation: b88-in .25s ease-out backwards; }
}
@container wall (364px <= width < 456px) {
  .wall:has(> .more:not([open])) > .b88:nth-child(n+13) { display: none; }
  .wall:has(> .more[open]) > .b88:nth-child(n+13) { animation: b88-in .25s ease-out backwards; }
}
@container wall (456px <= width < 548px) {
  .wall:has(> .more:not([open])) > .b88:nth-child(n+16) { display: none; }
  .wall:has(> .more[open]) > .b88:nth-child(n+16) { animation: b88-in .25s ease-out backwards; }
}
@keyframes b88-in { from { opacity: 0; transform: translateY(-3px); } }
@supports not selector(:has(*)) { .more { display: none !important; } }
@media (prefers-reduced-motion: no-preference) { .more .ic { transition: transform .15s; } }
@media (prefers-reduced-motion: reduce) { .wall > .b88 { animation: none !important; } }

/* My buttons, each with its code to copy. */
.mine-head { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; }
/* One code at a time (the details share a name), as a line under all the buttons; app.js re-lays the rack. */
.mine .items { display: flex; flex-wrap: wrap; gap: 10px 14px; position: relative; }
.mine .items:has(details[open]) { padding-bottom: 34px; }
.mine .item { display: grid; grid-template-columns: 88px; gap: 4px; justify-items: start; width: 88px; }
.mine .item img.b88 { image-rendering: pixelated; }
.mine .item .key { font-size: 9px; padding: 5px 6px; min-height: 22px; width: 100%; justify-content: center; }
.mine details summary { cursor: pointer; font-size: 8.5px; }
.mine details[open] .code { position: absolute; left: 0; right: 0; bottom: 0; }
.mine .code { width: 100%; font: 400 11px/1.3 ui-monospace, Consolas, monospace; background: var(--panel-hi); color: var(--ink); border: 1px solid var(--edge); border-radius: 3px; padding: 4px 6px; margin-top: 4px; }

/* units/ups.css */
/* The UPS is the site's footer. A sticker on the screen's corner. */
.ups .face { justify-content: space-between; gap: 10px 16px; padding-block: 12px; }
.ups .lcd { font-size: 20px; display: flex; gap: 12px; align-items: center; padding-right: 40px; }
.batt { display: inline-grid; grid-template-columns: repeat(5, 7px); gap: 2px; padding: 2px; border: 2px solid var(--lcd); position: relative; }
.batt::after { content: ""; position: absolute; right: -6px; top: 4px; width: 3px; height: 6px; background: var(--lcd); }
.batt i { height: 10px; background: var(--lcd); }
.batt i.e { background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--lcd) 35%, transparent); }
.batt.low i:not(.e) { background: var(--led-err); }
/* On battery: the screen blinks, like a real UPS beeping. */
@media (prefers-reduced-motion: no-preference) {
  .ups .lcd.alarm { animation: ups-alarm 1s steps(1) infinite; }
}
@keyframes ups-alarm { 50% { filter: brightness(.55); } }
.ups .sticker {
  position: relative; z-index: 3; align-self: flex-start;
  margin: -14px auto 0 -52px;
  font: 600 18px/1.05 var(--f-hand); color: var(--note-fg); background: var(--note-bg);
  padding: 6px 10px 8px; transform: rotate(-4deg); box-shadow: 0 6px 12px -6px #000c;
}
.ups .meta { display: grid; gap: 4px; font-size: 13px; color: var(--dim); }
.ups .meta b { color: var(--ink); font-weight: 600; }
@media (max-width: 560px) {
  .ups .lcd { font-size: 18px; padding-right: 10px; }
  .ups .sticker { margin: -4px 0 0 8px; }
}

/* units/lab.css */
/* The /lab rack: the Proxmox host and the containers, a unit each (templates/units/lab.html).
   The bars are segments like LED bar graphs: --p is the percentage filled. */
.meter { display: inline-grid; grid-template-columns: auto minmax(60px, 1fr) auto; align-items: center; gap: 8px; min-width: 0; }
.meter i {
  height: 10px; min-width: 60px;
  background: linear-gradient(90deg, var(--led-ok) calc(var(--p) * 1%), var(--led-off) 0);
  -webkit-mask: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 7px);
          mask: repeating-linear-gradient(90deg, #000 0 5px, transparent 5px 7px);
}
.meter.hot i { background: linear-gradient(90deg, var(--led-warn) calc(var(--p) * 1%), var(--led-off) 0); }
.meter b { font: 400 16px/1 var(--f-lcd); color: var(--ink); font-variant-numeric: tabular-nums; white-space: nowrap; }

.pve .face { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto; gap: 12px 24px; align-items: center; padding-block: 14px; }
.pve .ident { display: grid; gap: 4px; }
.pve .name { margin: 0; font: 700 30px/1 var(--f-silk); letter-spacing: .02em; }
.pve .meters { display: grid; gap: 10px; min-width: 0; }
.pve .meter { grid-template-columns: 64px minmax(60px, 1fr) 9em; }
.pve .meter b { text-align: right; }

.ct .face { display: grid; grid-template-columns: auto 64px minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1.6fr) 8em; gap: 14px; align-items: center; padding-block: 6px; }
.ct .name { font: 600 14px/1.2 var(--f-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ct .tape { justify-self: start; }
.ct .up { text-align: right; white-space: nowrap; }
.ct .up.off { grid-column: 4 / -1; color: var(--led-err); }
.ct.down .name, .ct.down .tape { opacity: .6; }
.ct.others .face { display: flex; justify-content: center; }
.ct.others .hand { font: 600 18px/1.1 var(--f-hand); color: var(--dim); }

/* Up to 960px the host's meters and a guest's two meters do not fit beside the rest (they ran into the names and
   the LCD): one under the other. */
@media (max-width: 960px) {
  .pve .face { grid-template-columns: minmax(0, 1fr); }
  .ct .face { grid-template-columns: auto 56px minmax(0, 1fr) auto; row-gap: 6px; padding-block: 8px; }
  .ct .meter { grid-column: 2 / -1; }
  .ct .up { grid-column: 4; grid-row: 1; }
  .ct .up.off { grid-column: 4; }
  /* The host's two meters on one grid: the values as wide as the widest (9em does not fit a phone), the bars level. */
  .pve .meters { grid-template-columns: auto minmax(40px, 1fr) auto; column-gap: 8px; }
  .pve .meter { grid-column: 1 / -1; grid-template-columns: 64px minmax(40px, 1fr) auto; grid-template-columns: subgrid; }
}
/* A phone: the uptime takes a line of its own, or it squeezes the guest's name out. */
@media (max-width: 400px) {
  .ct .up, .ct .up.off { grid-column: 2 / -1; grid-row: auto; text-align: left; }
}

/* Without power (assets/js/power.js) the bars go dark with the unit. */
.u.off .meter i { background: var(--led-off); }
.u.off .meter b, .u.off .ct .up { color: transparent; }

/* units/projects.css */
/* The /projects rack: a server per repository (templates/units/projects.html). The name on a label maker tape, the
   description and the topics as GitHub has them; the LCD with the latest release and the last commit, the keys. */
.prj .face { display: grid; grid-template-columns: minmax(0, 1fr) minmax(200px, 250px); gap: 12px 22px; align-items: center; padding-block: 12px; }
.prj .ident { display: grid; gap: 7px; align-content: center; }
.prj-name { margin: 0; line-height: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.prj-name .tape { font-size: 20px; padding: 5px 11px 4px; text-decoration: none; }
.prj-name a.tape:hover { filter: brightness(1.2); }
.prj-name .arch-tape { font-size: 10px; padding: 3px 7px 2px; }
.prj .desc { margin: 0; max-width: 62ch; color: var(--ink); text-wrap: pretty; }
.prj .topics .tape { font-size: 9.5px; padding: 2px 6px 1px; background: var(--key); color: var(--key-ink); text-shadow: none; box-shadow: inset 0 0 0 1px var(--edge); transform: none; text-transform: none; }
.prj .side { display: grid; gap: 8px; align-content: center; min-width: 0; }
.prj .side .lcd.two { font-size: 19px; }
.prj .side .ports { gap: 6px; }
.prj .side .key { flex: 1 1 0; justify-content: center; }

/* Up to 940px the screen and the keys go under the description, side by side. */
@media (max-width: 939px) {
  .prj .face { grid-template-columns: minmax(0, 1fr); }
  .prj .side { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .prj .side .prj-stats { grid-column: 1 / -1; grid-row: 2; }
}
/* A phone: the screen over the keys. */
@media (max-width: 560px) {
  .prj .side { grid-template-columns: minmax(0, 1fr); }
  .prj .side .prj-stats { grid-row: auto; }
  .prj-name .tape { font-size: 17px; }
}

/* drawer.css */
/* Units that pull out: the handle on the left ear and a drawer with the details under the unit. It slides
   out by itself (@starting-style); without JS it comes pulled out (?open=<unit>). */
.u > .pull {
  position: absolute; z-index: 3; left: 0; top: 50%; translate: 0 -50%;
  width: var(--ear); height: min(52px, 78%); display: grid; place-items: center; cursor: pointer;
}
.u > .pull::after { content: ""; position: absolute; inset: -4px -6px; } /* for a finger: 32px wide */
.u > .pull i {
  width: 8px; height: 100%; border-radius: 4px;
  background: linear-gradient(90deg, var(--screw), var(--screw-hi) 45%, var(--screw));
  box-shadow: 0 2px 3px #0008, inset 0 0 0 1px #0004;
  transition: translate .2s;
}
.u > .pull:hover i, .u > .pull[aria-expanded="true"] i { translate: -3px 0; background: linear-gradient(90deg, var(--accent), #fff 45%, var(--accent)); }
.u > .pull[aria-busy="true"] i { opacity: .6; }

/* A pulled-out unit stands slightly forward. */
.u:has(+ .drawer.open) { z-index: 2; box-shadow: 0 16px 22px -14px #000; }
.u:has(+ .drawer.open) .face { background: linear-gradient(var(--panel-hi), var(--panel-hi) 30%, var(--panel)); }

.drawer { display: none; }
.drawer.open {
  display: grid; grid-template-rows: 1fr; position: relative; z-index: 1;
  margin-inline: calc(var(--ear) * -.5);
  transition: grid-template-rows .35s cubic-bezier(.3, .7, .3, 1);
}
@starting-style { .drawer.open { grid-template-rows: 0fr; } }
.drawer.open.closing { grid-template-rows: 0fr; }
@media (prefers-reduced-motion: reduce) { .drawer.open { transition: none; } }
.drawer .slide {
  min-height: 0; overflow: hidden; display: grid; gap: 10px; justify-items: start;
  padding: 14px 18px; border-radius: 0 0 4px 4px;
  /* the slides on the sides and the drawer's grille bottom */
  background:
    linear-gradient(90deg, var(--rail-hi) 0 6px, transparent 6px calc(100% - 6px), var(--rail-hi) 0),
    repeating-linear-gradient(90deg, #ffffff06 0 2px, transparent 2px 8px),
    var(--void);
  box-shadow: inset 0 10px 14px -10px #000, 0 14px 22px -14px #000;
}
.drawer .slide > * { justify-self: stretch; }
.drawer .shut { justify-self: start; }
.detail { display: grid; gap: 10px; min-width: 0; }
.detail > .silk { margin: 0; }

/* The media server: the week in Koito. */
.media-week .stats { display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0; }
.media-week .tops { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px 24px; }
.media-week h3 { margin: 0 0 6px; font-size: 10px; }
.media-week ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.media-week li { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; grid-template-rows: auto auto; column-gap: 10px; align-items: center; }
.media-week li img, .media-week li .nocover { grid-row: span 2; width: 32px; height: 32px; border-radius: 2px; background: var(--panel-hi); object-fit: cover; }
.media-week li a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: none; }
.media-week li a:hover { text-decoration: underline; }
.media-week li small { grid-column: 2; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
/* The counter is a small LCD: its text stands only on the LCD's own background, whatever the cover's colours. */
.media-week li b {
  grid-column: 3; grid-row: 1 / span 2; font: 400 18px/1 var(--f-lcd); color: var(--lcd);
  background: var(--lcd-bg); padding: 3px 6px 2px; border-radius: 2px; box-shadow: inset 0 0 0 1px var(--edge);
}
.media-week li:not(:has(small)) a { grid-row: span 2; }

/* Monitoring: the bars of the last checks. */
.mon-history .hist { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.mon-history .hist li { display: grid; grid-template-columns: 110px minmax(0, 1fr) 17em; gap: 12px; align-items: center; }
.mon-history .who { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.mon-history .who .silk { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mon-history .bars { display: flex; align-items: flex-end; gap: 1px; height: 22px; min-width: 0; }
.mon-history .bars i {
  flex: 1; min-width: 1px; border-radius: 1px 1px 0 0; background: var(--led-ok); opacity: .8;
  height: calc(max(2px, var(--ms) / max(var(--max), 1) * 100%));
}
.mon-history .bars i.x { height: 100%; background: var(--led-err); opacity: 1; }
.mon-history li.warn .bars i:not(.x) { background: var(--led-warn); }
.mon-history .nums { font-size: 12px; color: var(--dim); white-space: nowrap; font-variant-numeric: tabular-nums; text-align: right; }
@media (max-width: 560px) {
  .drawer .slide { padding: 12px 12px; }
  .mon-history .hist { gap: 8px; }
  .mon-history .hist li { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
  .mon-history .bars { grid-column: 1 / -1; grid-row: 2; }
}

/* toys.css */
/* Toys (assets/js/toys.js): the BIOS on the KVM, RESET and UID on the server, the BIOS settings (rainbow, steady light). */

/* ---------- BIOS Setup over the console ---------- */
.u.kvm { position: relative; }
.bios {
  position: absolute; inset: 8px 10px; z-index: 5; overflow: auto; padding: 10px 14px; border-radius: 3px;
  background: #0000a8; color: #c0c0c0; box-shadow: inset 0 0 0 2px #5555ff, 0 0 24px #0008;
  font: 400 15px/1.35 ui-monospace, "Cascadia Mono", Consolas, monospace; text-transform: uppercase;
}
.bios p { margin: 0; }
.bios .b-title { text-align: center; color: #fff; background: #00a8a8; margin: -10px -14px 6px; padding: 3px; }
.bios .b-tabs { color: #a8a8ff; letter-spacing: .08em; word-spacing: 1.2em; }
.bios .b-tabs b { color: #0000a8; background: #c0c0c0; padding: 0 6px; font-weight: 400; }
.bios .b-rows { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 16px; margin: 10px 0; }
.bios dt { color: #c0c0c0; }
.bios dd { margin: 0; color: #fff; text-align: right; white-space: nowrap; }
.bios button { all: unset; cursor: pointer; color: #ffff55; }
.bios button:focus-visible, .bios button:hover { background: #c0c0c0; color: #0000a8; }
.bios .b-help { color: #a8a8ff; border-top: 1px solid #5555ff; padding-top: 6px; font-size: 13px; }
@media (max-width: 560px) { .bios { font-size: 12.5px; padding: 8px 10px; } .bios .b-title { margin: -8px -10px 6px; } }

/* ---------- the server: UID and RESET ---------- */
.srv .leds { align-items: center; }
/* button.uid, not .uid: the lamp next to it is .uid too, and all: unset would reset it. */
.srv button.uid {
  all: unset; cursor: pointer; font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
}
.srv button.uid:focus-visible, .srv .rst:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.led.uid { background: var(--led-off); box-shadow: inset 0 1px 1px #0006; }
.u.uid-on .led.uid { background: #3d8bff; box-shadow: 0 0 8px #3d8bff; }
/* "Find me" shows from afar, as on a real server: a blue beacon along the unit's edge. */
.u.uid-on .face { box-shadow: inset 0 0 0 2px #3d8bff99, inset 0 0 22px #3d8bff33; }
@media (prefers-reduced-motion: no-preference) {
  .u.uid-on .led.uid { animation: uid-blink 1s steps(2, jump-none) infinite; }
  .u.uid-on .face { animation: uid-glow 2s ease-in-out infinite; }
}
@keyframes uid-glow { 50% { box-shadow: inset 0 0 0 2px #3d8bff33, inset 0 0 8px #3d8bff11; } }
@keyframes uid-blink { 50% { background: var(--led-off); box-shadow: none; } }
/* The RESET hole: pressed with a "paper clip", a crosshair cursor. */
.srv .rst {
  width: 7px; height: 7px; padding: 0; border: 0; border-radius: 50%; cursor: crosshair;
  background: #050506; box-shadow: inset 0 1px 2px #000, 0 0 0 2px var(--panel-hi);
}

/* ---------- POST (toys.js) ----------
   A unit booting from dark is .posting for a couple of seconds: each screen with data-post shows its boot (data-frame)
   over what it holds, like the "no link" notice (network.css: that one waits for the boot), and the unit's lamps go
   through a test of their own. */
.u .post { position: relative; overflow: hidden; color: transparent; text-shadow: none; }
.u .post > * { visibility: hidden; }
.u .post::after {
  content: attr(data-frame); position: absolute; inset: 0; padding: inherit; white-space: pre-line; text-align: left;
  font: inherit; line-height: 1.1; color: var(--lcd); text-shadow: 0 0 6px color-mix(in srgb, var(--lcd) 45%, transparent);
}
.u .lcd.post::after { font-size: min(18px, 1em); }
.rtr .lcd.post::after { white-space: pre; }
.kvm .screen.post::after { font: 400 19px/1.2 var(--f-lcd); letter-spacing: .04em; color: var(--crt-ink); text-shadow: 0 0 6px color-mix(in srgb, var(--crt-ink) 33%, transparent); }
/* A guest's uptime: the frames grow out of its right edge (a phone: its left), in amber. */
.ct .up.post { overflow: visible; }
.ct .up.post::after { left: auto; width: max-content; color: var(--led-warn); text-shadow: none; }
@media (max-width: 400px) { .ct .up.post::after { left: 0; right: auto; } }
/* Lamps that blink while the unit boots: the router's WAN and LAN, the server's disk, a guest's. */
.rtr.posting .led:is(.wan, .lan), .ct.posting > .face > .led { background: var(--led-warn); box-shadow: 0 0 6px var(--led-warn); }
.srv.posting .led.hdd { background: var(--led-ok); box-shadow: 0 0 6px var(--led-ok); }
@property --p { syntax: "<number>"; inherits: true; initial-value: 0; } /* lab.css: a meter's fill, swept below */
@media (prefers-reduced-motion: no-preference) {
  .rtr.posting .led:is(.wan, .lan), .ct.posting > .face > .led { animation: post-blink .3s steps(1) infinite; }
  .rtr.posting .led.lan { animation-delay: .15s; }
  .srv.posting .led.hdd { animation: post-blink .14s steps(1) infinite; }
  /* The switch: every port lamp at once, then a run along the ports; the contacts switch: one port after another. */
  .tor.posting :is(.tp, .sfp-port) .led { animation: post-lamp .5s steps(1), post-lamp .2s steps(1) calc(.7s + var(--i) * 40ms); }
  .sw.posting .port .led { --lamp: var(--led-ok); animation: post-lamp .25s steps(1) calc(var(--i) * 110ms); }
  /* Management: the keys' backlight, three times. */
  .mgmt.posting .key { animation: post-keys .5s 3; }
  /* The player: the VU meter up to the top and down again, the cover loading line by line. */
  .media.posting .vu i { --lamp: var(--led-ok); animation: post-lamp calc(1.9s - var(--i) * 120ms) steps(1) calc(var(--i) * 60ms); }
  .media.posting .vu i.w { --lamp: var(--led-warn); }
  .media.posting .vu i.e { --lamp: var(--led-err); }
  .media.posting .tray { animation: post-scan 1.4s steps(7) .7s backwards; }
  /* Monitoring: each check probed in turn. */
  .mon.posting .cell .led { animation: post-probe .35s steps(1) calc(.5s + var(--i) * 60ms) backwards; }
  /* The guestbook's CRT warms up. */
  .kvm.posting .screen { animation: post-crt .5s ease-out; }
  /* Proxmox and its guests: the meters swing to full and back to their value. */
  :is(.pve, .ct).posting .meter i { animation: post-needle 1.4s ease-in-out .3s backwards; }
}
@keyframes post-blink { 50% { background: var(--led-off); box-shadow: none; } }
@keyframes post-lamp { from, to { background: var(--lamp, var(--led-warn)); box-shadow: 0 0 6px var(--lamp, var(--led-warn)); } }
@keyframes post-probe { from { background: var(--led-off); box-shadow: none; } 40%, to { background: var(--led-warn); box-shadow: 0 0 6px var(--led-warn); } }
@keyframes post-keys { 0%, 50% { filter: brightness(1.6) drop-shadow(0 0 4px var(--accent)); } 51%, 100% { filter: none; } }
@keyframes post-scan { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0); } }
@keyframes post-crt { from { transform: scaleY(.02); filter: brightness(4); } 60% { transform: none; filter: brightness(1.6); } }
@keyframes post-needle { from { --p: 0; } 50% { --p: 100; } }

/* ---------- BIOS settings ---------- */
/* Rainbow: the green LEDs shimmer ("20% cooler"). */
@media (prefers-reduced-motion: no-preference) {
  :root.rainbow .led:not(.off, .warn, .err, .pwr, .uid), :root.rainbow :is(.tp, .sfp-port).link .led { animation: rainbow 2.4s linear infinite !important; }
}
@keyframes rainbow { to { filter: hue-rotate(360deg); } }
/* Steady light: the LEDs do not blink, neither at power-up nor from traffic (live.js sets no .tx then either). */
:root.steady .led, :root.steady .hall .hw > i { animation: none !important; }

/* cables.css */
/* assets/js/app.js draws the cables in an SVG over the room. Without JS there are none. */
.cables { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; overflow: visible; }
.cables .lbl { font: 600 18px var(--f-hand); fill: var(--dim); }
.cables .outlet { fill: var(--panel-hi); stroke: var(--edge); }
.cables .outlet-hole { fill: var(--hole); }
/* The fibre's entry into the yellow duct on the ceiling (app.js, fiber). */
.cables .inlet { fill: #202328; stroke: #000b; stroke-width: 1.5; stroke-linejoin: round; }
/* Patch cords that pull out (net.js): pulled ones hang over the cabling, under the power cables. */
.cables.net { z-index: 5; }
.cables.net .hit { fill: transparent; pointer-events: auto; cursor: grab; touch-action: manipulation; -webkit-touch-callout: none; user-select: none; }
.cables.net .hit:active { cursor: grabbing; }

/* The rack has arrived (aisle.js -> app.js, .draw): the cords are drawn one by one; --i is the wire's number,
   --k its plug (0 at the start, 1 at the end). */
@media (prefers-reduced-motion: no-preference) {
  .cables.draw > g path[pathLength] {
    stroke-dasharray: 100 100;
    animation: cable-draw .55s cubic-bezier(.45, 0, .2, 1) calc(var(--i, 0) * 70ms) both;
  }
  .cables.draw > rect, .cables.draw > text, .cables.draw > .inlet {
    transform-box: fill-box; transform-origin: center;
    animation: cable-plug .3s cubic-bezier(.3, 1.6, .5, 1) calc(var(--i, 0) * 70ms + var(--k, 0) * 480ms) both;
  }
}
@keyframes cable-draw { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
@keyframes cable-plug { from { opacity: 0; transform: scale(.3); } }
.cables .lbl.small { font-size: 16px; }

/* The cable tester (net.js, power.js): a hovered plug's cord glows and its port's and card's lamps blink. */
.cables g.probe { filter: brightness(1.35) drop-shadow(0 0 4px rgba(255, 255, 255, .5)); }
@media (prefers-reduced-motion: no-preference) {
  :is(.tp, .sfp-port).link.probe .led, .nic.probe .led { animation: probe-blink .14s steps(2, jump-none) infinite !important; }
}
@keyframes probe-blink { 50% { opacity: .15; } }

/* Live guests (live.js): a new guest's cord from the patch panel to the switch; packets are dots along the cords. */
@media (prefers-reduced-motion: no-preference) {
  .cables > g.fresh path { stroke-dasharray: 100 100; animation: cable-draw .7s cubic-bezier(.45, 0, .2, 1) both; }
}
.cables.packets { z-index: 5; }
.cables .packet { fill: #fff; filter: drop-shadow(0 0 4px #8ecbff) drop-shadow(0 0 2px #fff); transition: opacity .3s; }
/* The background chatter's packets (live.js): smaller and dimmer, so the real ones, which carry data, stand out. */
.cables .packet.bg { opacity: .75; filter: drop-shadow(0 0 2px #8ecbff) drop-shadow(0 0 1px #fff); }
.cables .packet.done { opacity: 0; }

/* power.css */
/* The units' power (assets/js/power.js): an inlet on every unit, the cables to the PDU, a dark unit, the UPS's button. */
.psu {
  position: absolute; z-index: 2; right: calc(var(--ear) + 2px); top: 50%; transform: translateY(-50%);
  width: 13px; height: 11px; border-radius: 2px;
  background: var(--hole); box-shadow: inset 0 0 0 1px #000, 0 0 0 1px var(--panel-hi);
}
.u:is(.has-psu, [data-host]) .face { padding-right: 36px; } /* a guest keeps its host's inset */
/* The UPS's own inlet: low on its left, on the wall socket's side; its cord is plugged in there. */
.ups > .psu { left: calc(var(--ear) + 6px); right: auto; top: auto; bottom: 14px; transform: none; }
.ups:has(> .psu) .face { padding-left: 36px; }
.cables.power { z-index: 6; }
.cables.power .hit { fill: transparent; pointer-events: auto; cursor: grab; touch-action: manipulation; -webkit-touch-callout: none; user-select: none; }
.cables.power .hit:active { cursor: grabbing; }

/* A unit without power: dim, its screens empty, its lamps dark; its drawer the same. */
.u.off .face, .u.off > .pull, .drawer.off .slide { filter: saturate(.2) brightness(.6); }
.u.off .lcd, .u.off .lcd *, .drawer.off .lcd, .drawer.off .lcd * { color: transparent !important; text-shadow: none !important; }
.u.off .led, .u.off .vu i, .drawer.off .led { background: var(--led-off) !important; box-shadow: inset 0 1px 1px #0006 !important; animation: none !important; }
.u.off img, .drawer.off img { filter: grayscale(1) brightness(.4); }
/* No covers without power: the tray is empty, as when there is no cover. */
.u.off img.tray, .drawer.off .media-week img { object-position: 200px 0; }
.u.off img.tray { background: repeating-linear-gradient(135deg, var(--void) 0 6px, var(--panel) 6px 12px); }
/* And it cannot be pressed (power.js sets inert; pointer-events for browsers without it). The UPS is never
   inert: its button turns the rack on. */
.u[inert], .drawer[inert] { pointer-events: none; user-select: none; cursor: default; }
/* The focus from a dark unit goes to the UPS (or the rack). */
.u.ups:focus-visible, .rack:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.u.off .screen { position: relative; }
.u.off .screen > * { visibility: hidden; }
.u.off .screen::after {
  content: attr(data-nosignal); position: absolute; inset: 0; display: grid; place-items: center;
  font: 500 12px/1 var(--f-silk); letter-spacing: .2em; text-transform: uppercase; color: var(--crt-faint);
}
@media (prefers-reduced-motion: no-preference) {
  .u .face, .u > .pull, .drawer .slide { transition: filter .25s; }
  .u.booting .face { animation: boot .7s steps(5); }
}
@keyframes boot { 0% { filter: brightness(1.9); } 30% { filter: brightness(.5); } 60% { filter: brightness(1.4); } 100% { filter: none; } }

/* The UPS's power button: green, red with the rack off. */
.ups .pwr {
  width: 34px; height: 34px; flex: none; border-radius: 50%; cursor: pointer;
  border: 2px solid var(--edge); background: radial-gradient(circle at 50% 40%, var(--panel-hi), var(--key));
  color: var(--led-ok); font: 700 15px/1 var(--f-silk); display: inline-grid; place-items: center; padding: 0;
  box-shadow: 0 2px 0 var(--edge), inset 0 1px 0 var(--panel-hi);
}
.ups .pwr:active { transform: translateY(1px); }
.ups .pwr .ic { width: 18px; height: 18px; stroke-width: 2.6; filter: drop-shadow(0 0 3px currentColor); }
.rack.powered-off .ups .pwr { color: var(--led-err); }

/* A sticker came unstuck (sticker.js): a copy falls to the floor; a tap puts it back. */
.peeled { visibility: hidden; }
.fallen-sticker {
  position: absolute; z-index: 5; left: 0; top: 0; margin: 0; max-width: none; box-sizing: border-box;
  pointer-events: none; transform-origin: 50% 50%; will-change: transform; user-select: none;
}
.fallen-sticker.on-floor { pointer-events: auto; cursor: pointer; filter: brightness(.9) saturate(.9); }
.fallen-sticker.on-floor:hover { filter: none; }
.fallen-sticker.returning { transition: transform .7s cubic-bezier(.3, .7, .3, 1); }

/* The hall's breaker (power.js): up, the row's sockets have current; down, every UPS is on battery. It, the
   light switch and the torch stand over the cable layers (z-index 7). */
.breaker {
  position: absolute; z-index: 7; display: grid; justify-items: center; gap: 4px;
  padding: 0; border: 0; background: none; cursor: pointer; color: var(--dim);
  font: 600 10px/1 var(--f-silk); letter-spacing: .14em; text-transform: uppercase;
}
.breaker .box {
  position: relative; width: 34px; height: 46px; border-radius: 4px;
  background: linear-gradient(#8f949b, #6d7279);
  box-shadow: inset 0 0 0 1px #0006, inset 0 1px 0 #fff4, 0 3px 6px #0006;
}
.breaker .box::before { content: ""; position: absolute; left: 12px; right: 12px; top: 7px; bottom: 7px; border-radius: 3px; background: #1b1d21; }
.breaker .handle {
  position: absolute; left: 8px; right: 8px; top: 8px; height: 14px; border-radius: 3px;
  background: #2bb35a; box-shadow: inset 0 -2px 0 #0004, 0 1px 2px #0008; transition: top .15s, background-color .15s;
}
.breaker.down .handle { top: 24px; background: #d6453d; }
.breaker:hover .box { filter: brightness(1.08); }
.breaker:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { .breaker .handle { transition: none; } }

/* The UPS's test button (power.js, the self-test): a small key next to the power button. */
.ups .utest {
  flex: none; align-self: center; padding: 5px 8px; border-radius: 3px; cursor: pointer;
  border: 1px solid var(--edge); background: var(--key); color: var(--key-ink);
  font: 600 9px/1 var(--f-silk); letter-spacing: .14em; text-transform: uppercase;
  box-shadow: inset 0 1px 0 var(--panel-hi), 0 2px 0 var(--edge);
}
.ups .utest:active { transform: translateY(1px); box-shadow: inset 0 1px 0 var(--panel-hi), 0 1px 0 var(--edge); }
.ups .utest:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The light switch (power.js): auto, day, evening, night. It and the torch hang in the top right corner: the top
   left is the cable's (the fibre goes up there with its caption) and the row's arrow back. */
.lightsw {
  position: absolute; z-index: 7; top: 40px; right: 22px; display: grid; justify-items: center; gap: 3px;
  padding: 0; border: 0; background: none; cursor: pointer; color: var(--dim);
  font: 600 9px/1 var(--f-silk); letter-spacing: .12em; text-transform: uppercase;
}
.lightsw .plate {
  position: relative; width: 26px; height: 34px; border-radius: 4px;
  background: linear-gradient(#e9e6df, #cfcbc2); box-shadow: inset 0 0 0 1px #0003, 0 2px 5px #0006;
}
.lightsw .rocker {
  position: absolute; left: 7px; right: 7px; top: 6px; bottom: 6px; border-radius: 2px;
  background: linear-gradient(#fbfaf7 50%, #d8d4cb 50%); box-shadow: inset 0 0 0 1px #0002;
}
.lightsw[data-mode="night"] .rocker, .lightsw[data-mode="evening"] .rocker { background: linear-gradient(#d8d4cb 50%, #fbfaf7 50%); }
.lightsw[data-mode="auto"] .plate::after {
  content: ""; position: absolute; right: 3px; top: 3px; width: 4px; height: 4px; border-radius: 50%;
  background: var(--led-ok); box-shadow: 0 0 4px var(--led-ok);
}
.lightsw .lbl { display: grid; text-align: center; }
.lightsw .lbl > span { grid-area: 1 / 1; visibility: hidden; }
.lightsw .lbl > span.on { visibility: visible; }
.lightsw:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
@media (max-width: 760px) { .lightsw { top: 30px; right: 12px; } }
/* A phone: the hall hardly shows around the rack, the light would have nothing to light. */
@media (max-width: 560px) { .lightsw { display: none; } }

/* The torch (live.js); taken, an empty hook hangs there. Left of the light switch with room for its longest caption
   ("evening": the switch is 46px wide). */
.torch {
  position: absolute; z-index: 7; top: 46px; right: 82px; display: grid; justify-items: center; gap: 3px;
  padding: 0; border: 0; background: none; cursor: pointer; color: var(--dim);
  font: 600 9px/1 var(--f-silk); letter-spacing: .12em; text-transform: uppercase;
}
.torch .shell {
  position: relative; width: 12px; height: 30px; border-radius: 3px 3px 2px 2px;
  background: linear-gradient(90deg, #3a3e45, #5d636c 45%, #2b2e33); box-shadow: 0 2px 4px #0007;
  transition: opacity .2s;
}
.torch .shell::before { /* the hook */
  content: ""; position: absolute; left: 4px; top: -6px; width: 4px; height: 6px; border: 1.5px solid #7d838c; border-bottom: 0; border-radius: 3px 3px 0 0;
}
.torch .lens { position: absolute; left: -2px; right: -2px; top: -1px; height: 7px; border-radius: 3px; background: radial-gradient(#fff6d8, #c9b37a); box-shadow: 0 0 0 1px #0006; }
.torch[aria-pressed="true"] .shell { opacity: .18; }
.torch:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
@media (max-width: 760px) { .torch { top: 36px; right: 72px; } }
@media (max-width: 560px) { .torch { right: 12px; } } /* no light switch on a phone */
/* A touch screen has no cursor to follow: no torch; the others' spots still shine. */
@media (hover: none) { .torch { display: none; } }

/* The light spots: my torch and the other guests' torches on this rack. */
.beam {
  position: absolute; z-index: 7; width: 200px; height: 200px; margin: -100px 0 0 -100px; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, color-mix(in srgb, rgba(255, 196, 92, .38) var(--L), rgba(255, 244, 214, .42)),
    color-mix(in srgb, rgba(255, 210, 120, .12) var(--L), rgba(255, 244, 214, .14)) calc(40% * var(--light) + 38% * (1 - var(--light))), transparent 68%);
  mix-blend-mode: var(--beam-blend); transition: opacity .35s;
}
.beam:not(.me) { transition: left .12s linear, top .12s linear, opacity .35s; }
.beam.out { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .beam:not(.me) { transition: none; } }
