/* СОБРАННЫЙ ФАЙЛ — правки затрёт следующая сборка.
 * Источники: tools/board/styles/base.css и tools/board/blocks/*.css,
 * собирает tools/build.py. Стили узла лежат рядом с его геометрией.
 */
/* Server schematic — the interactive half of the personal card.
 *
 * The prototype and the board generator live in server-proto: 32 memory slots,
 * eight fans and the silkscreen are put together by a script, nobody writes
 * that by hand.
 *
 * Two levels. Normal: the callout labels are visible at once, the units work
 * as links. Service (the SERVICE switch on the board): units come out, the
 * console appears, the diagnostics panel slides out.
 *
 * About fills. The schematic once lost its background entirely, and the blame
 * went first to gradients, then to opacity on SVG nodes. Neither was it: a
 * rule like `.fault { fill-opacity: 0 }` matched .rig itself, and fill-opacity
 * is inherited by every descendant. That is why the lamp selectors are
 * narrowed to circle.fault, and why we put lamps out through fill-opacity and
 * not opacity — opacity lifts a node into a layer of its own. Gradients do
 * work meanwhile: url(#die-shine) runs across the CPU die right inside the
 * tilted stage.
 */

/* ── Two views of one card ────────────────────────────────────────────────
 * The card with the list of links, and the server schematic. A button next to
 * the theme one switches them, the choice is remembered. body centres them —
 * it is a centring flex already.
 */
body.view-card .rig { display: none; }
body.view-rig main { display: none; }
/* display only: .rig sets its own width. There used to be width: 100% here,
   and on specificity it beat the ceiling inside .rig — however much you edited
   that ceiling, the machine stretched across the whole screen. */
body.view-rig .rig { display: block; }

/* The view button is the theme switch's own sister: same size, same inverse
   backing, sitting to its left. Otherwise it gets lost against the page. */
.view-switch {
    position: fixed;
    top: 24px;
    right: 78px;
    z-index: 4;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border-radius: 14px;
    border: 1px solid var(--inv-line);
    background: var(--inv-surface);
    box-shadow: var(--inv-shadow);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    animation: contentEnter 0.65s cubic-bezier(0.32, 0.72, 0, 1) 680ms backwards;
    transition:
        background   var(--theme-fade),
        border-color var(--theme-fade),
        box-shadow   var(--theme-fade),
        transform    0.44s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* The reassemble button: the same tile as the view and theme ones, third in
   the row. Shown on the schematic only — in card view there is nothing to
   reassemble. */
.assemble-btn {
    position: fixed;
    top: 24px;
    right: 132px;
    z-index: 4;
    display: none;
    place-items: center;
    width: 42px;
    height: 42px;
    padding: 0;
    border-radius: 14px;
    border: 1px solid var(--inv-line);
    background: var(--inv-surface);
    box-shadow: var(--inv-shadow);
    color: var(--inv-node);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition:
        background   var(--theme-fade),
        border-color var(--theme-fade),
        box-shadow   var(--theme-fade),
        transform    0.44s cubic-bezier(0.34, 1.56, 0.64, 1);
}
body.view-rig .assemble-btn { display: grid; }
.assemble-btn svg { width: 21px; height: 21px; display: block; }
.assemble-btn:hover { transform: translate3d(0, -1px, 0); }
/* While the assembly runs the button is released and does not fire again: a
   second run on top of the first would knock the schedule out, and some of
   the units would stay outside the machine. */
.rig.assembly ~ .assemble-btn,
body:has(.rig.assembly) .assemble-btn { opacity: 0.45; pointer-events: none; }
.assemble-btn:active svg { transform: rotate(-40deg); }
.assemble-btn svg { transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); }
.assemble-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.view-switch:hover { transform: translate3d(0, -1px, 0); }
.view-switch:active { transform: translate3d(0, 0, 0) scale(0.96); }
.view-switch:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.view-switch svg {
    grid-area: 1 / 1;
    width: 22px;
    height: 22px;
    display: block;
    color: var(--inv-node);
    transition:
        opacity   0.35s ease,
        transform 0.55s cubic-bezier(0.34, 1.56, 0.64, 1),
        color     var(--theme-fade);
}

/* We show where the click leads: in card view — the rack, and the reverse. */
.icon-card { opacity: 0; transform: rotate(-90deg) scale(0.5); }
.icon-rack { opacity: 1; transform: none; }
body.view-rig .icon-rack { opacity: 0; transform: rotate(90deg) scale(0.5); }
body.view-rig .icon-card { opacity: 1; transform: none; }

/* The schematic needs colour roles of its own: the card names its tokens
   differently. */
.rig {
  --ink:  #93a1a1;
  --dim:  #586e75;
  --head: #eee8d5;
  --green: #859900;
  --cyan:  #2aa198;
  --amber: #b58900;
  --red:   #dc322f;
  --edge: rgba(147, 161, 161, 0.22);
  --fade: 0.45s cubic-bezier(0.32, 0.72, 0, 1);
  /* The spring is left for the small parts — latches and tabs. Anything big
     travels on --glide: a long ease-out with no bounce. The heatsink is the
     size of the socket, and a spring settle on it read as a jerk rather than
     as a hand moving. */
  --spring: 0.6s cubic-bezier(0.34, 1.3, 0.64, 1);
  --glide: 0.72s cubic-bezier(0.22, 1, 0.36, 1);
  /* Switching the mode is the largest movement on the page: a column opens
     up, the machine shrinks, the instruments slide in. On --glide it read as
     a jerk, and not because the curve is bad: in the same three quarters of a
     second the whole composition travels here, not a single unit. A second
     and a half on the same bounce-free curve — starts at once, brakes for
     a long time. */
  --svc: 1.5s cubic-bezier(0.22, 1, 0.36, 1);
  /* The click: a latched unit refuses to move at first, then breaks free and
     runs on freely, slowing down. That is how a DIMM, a fan, a riser and a
     power supply come out — everything that is held by a catch. The CPU does
     not move that way: it is not yanked but lifted, so it keeps --glide.

     For a third of the time the unit stands almost still, over the next seven
     per cent it covers two thirds of the distance — that is the break-free —
     and the remainder is settled barely visibly. The gap between the yank and
     the settling is kept large on purpose: on a soft curve the click just
     reads as a fast movement. */
  --click: 0.9s linear(
      0, 0.003 14%, 0.009 24%, 0.018 31%, 0.30 35%, 0.66 39%, 0.83 44%,
      0.90 52%, 0.945 63%, 0.975 76%, 0.993 88%, 1);

  /* The tempo of the indication. Kept in one place: lamps blink often so the
     machine looks alive. Both of these are stepped rather than smooth —
     measured, a smooth blink costs a quarter more, because it asks for a
     repaint on every frame the monitor draws. The fans used to be in the same
     boat and are not any more: they turn on a layer of their own, where the
     compositor does the work. */
  --blink: 1.4s;
  --beat: 1s;
  /* Half a second a turn for the wall — this is a machine at its rated rpm,
     and at that speed nobody sees blades anyway, only the disc. The fan inside
     a power supply keeps a second: it cools itself alone off standby power,
     turns visibly slower, and is the one place on the board where an impeller
     can still be read as an impeller while the machine runs.

     The Efficiency profile stretches the wall to two seconds (screen.css), and
     that is where the blades come back. No period has to divide into whole
     seconds any more — that rule came from the repaint grid, and the fans left
     it for the compositor. */
  --spin: 0.5s;
  --spin-aux: 1s;
  /* The blink periods still fall on a grid of a twentieth of a second: 1.4 s
     divides into 28 positions, the pulse and the blink into round fractions. */

  /* Seating. Every unit has a character of its own, and that is not
     decoration: a DIMM is pressed home until the latches click, a fan goes
     stiffly down its guides, a power supply flies into its bay in one motion,
     and a drive first slides freely and only meets resistance at the very
     end. */
  --press-hard: 0.95s linear(
      0, 0.20 10%, 0.40 20%, 0.53 29%, 0.585 38%, 0.60 48%, 0.615 57%,
      0.68 64%, 0.88 71%, 1.03 78%, 1.01 86%, 1);
  --press-snap: 0.62s linear(
      0, 0.05 8%, 0.22 16%, 0.62 24%, 0.94 32%, 1.03 38%, 0.995 48%, 1);
  --press-seat: 0.9s linear(
      0, 0.30 12%, 0.55 24%, 0.70 36%, 0.74 46%, 0.755 58%, 0.78 66%,
      0.94 76%, 1.02 84%, 1);

  position: relative;
  z-index: 1;
  margin: 0 auto;
  width: 100%;
  padding: 0 12px;
  /* The size of the machine. The height limit is the main one: the schematic
     is wide and low, and it runs into the screen by height, not by width.
     1.66 is the viewBox ratio, and the viewBox is symmetric about the
     chassis: exactly 120 units left and right, 36 top and bottom. The margin
     on the left is there for the drive that slides out, and while there was
     none on the right the chassis stood seventy-six units right of centre —
     to the eye that read as "the machine is off centre". The top and bottom
     margins appeared for the rack ears: they stick out sideways, past the
     side walls. */
  --rig-w: min(68vh * 1.66, 1200px);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.rig *, .rig *::before, .rig *::after { box-sizing: border-box; }

/* Name and status are visible in both views — this is a personal card, not
   only a schematic. */
.rig-id { text-align: center; margin: 0 auto 18px; }
.rig-id h2 { margin: 0 0 4px; font-size: 1.5rem; font-weight: 600;
             letter-spacing: -0.02em; color: var(--heading); }
.rig-id .bio { margin: 0 0 6px; font-size: 0.95rem; color: var(--bio); }
.rig-id .now { margin: 0; font-size: 0.8rem; color: var(--now); }
.rig-id .now .live-dot { margin-right: 7px; }

/* The console is part of service mode, below the machine. */
.rig .panel {
  display: none;
  margin: 0;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: linear-gradient(180deg, rgba(9,34,40,0.92) 0%, rgba(7,26,31,0.88) 100%);
  padding: 10px 12px 8px;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.82rem;
  line-height: 1.55;
}
.rig.service .panel { display: block; }
/* The console heading is its nameplate: the word console, the machine model,
   the uptime. The model belongs here and not in the log: the log is for
   events, while the machine's passport is permanent. All grey, at different
   brightness; a long spec is clipped rather than wrapped — the heading has to
   stay on one line at any width. */
.rig .panel h2 {
  margin: 0 0 8px;
  font-size: 0.68rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim);
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  /* The heading is itself a flex item of the column, and its min-width: auto
     pushed the panel apart from the inside: the spec does not wrap, so
     min-content equals the whole line. Zero brings the ellipsis back. */
  min-width: 0;
}
.con-name { flex: none; }
.con-spec {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-transform: none; letter-spacing: 0.02em; font-weight: 500;
  color: rgba(88, 110, 117, 0.85);
}
.con-up { flex: none; font-variant-numeric: tabular-nums; }
.console-log { height: 132px; overflow-y: auto; overscroll-behavior: contain;
               display: flex; flex-direction: column; gap: 2px; }
.console-log div { white-space: pre-wrap; word-break: break-word; }
.console-log .ok { color: var(--green); }
.console-log .warn { color: var(--amber); }
.console-log .err { color: var(--red); }
.console-log .muted { color: var(--dim); }
.console-log::-webkit-scrollbar { width: 6px; }
.console-log::-webkit-scrollbar-thumb { background: rgba(147,161,161,0.28); border-radius: 3px; }

.prompt { display: flex; align-items: baseline; gap: 6px; margin-top: 8px;
          border-top: 1px solid rgba(147,161,161,0.14); padding-top: 7px; }
.ps1 { color: var(--green); white-space: nowrap; }
.prompt input { flex: 1; min-width: 0; background: none; border: 0; outline: none;
                color: var(--head); font: inherit; caret-color: var(--cyan); }
.prompt input::placeholder { color: rgba(88,110,117,0.75); }


  /* The prompt: the console does not only print, you can type into it. */
  .prompt { display: flex; align-items: baseline; gap: 6px; margin-top: 8px;
            border-top: 1px solid rgba(147,161,161,0.14); padding-top: 7px; }



  /* ── Panels ───────────────────────────────────────────────────────────── */
  .panel {
    border: 1px solid var(--edge);
    border-radius: 12px;
    background: linear-gradient(180deg, rgba(9,34,40,0.92) 0%, rgba(7,26,31,0.88) 100%);
    padding: 12px 14px 14px;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.82rem;
    line-height: 1.55;
  }

  .sensor { display: flex; justify-content: space-between; gap: 8px; padding: 3px 0; }
  .sensor span:last-child { color: var(--head); }
  .sensor.off span:last-child { color: var(--dim); }

  .bar { height: 3px; border-radius: 2px; background: rgba(147,161,161,0.14); overflow: hidden; margin: 2px 0 8px; }
  .bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--cyan), var(--green)); width: 0; transition: width 0.9s ease; }

  /* ── Stage ────────────────────────────────────────────────────────────── */
  /* The tilt is back: the disappearing fills came not from the 3D but from
     the gradients — the paint server fell off when the group was transformed.
     There are no gradients left in the SVG, so perspective is safe. */
  .stage { position: relative; perspective: 1700px; perspective-origin: 50% 42%;
           display: flex; justify-content: center; }
  .stage > .chassis { width: 100%; margin: 0 auto; position: relative; z-index: 1; }

  /* The same coloured halo as under the card of links: the machine must not
     hang on a bare background while the card has one. The spread is smaller —
     the stage is twice as wide as the card to begin with, and at the full
     blur(160px) the glow would crawl across the whole screen. */
  .rig-glow {
    position: absolute; inset: 4% 12%;
    border-radius: 80px;
    background: linear-gradient(45deg,
        rgba(255, 50, 100, 0.45) 0%,
        rgba(255, 80, 150, 0.30) 25%,
        rgba(120, 40, 220, 0.18) 50%,
        rgba(0, 220, 255, 0.30) 75%,
        rgba(0, 255, 255, 0.45) 100%);
    filter: blur(130px);
    pointer-events: none;
    z-index: 0;
    opacity: 0;
    transition: opacity 1.1s ease;
  }
  .rig.ready .rig-glow { opacity: 1; }

  .chassis {
    position: relative;
    transform: rotateX(46deg) scale(0.94);
    will-change: transform;
    backface-visibility: hidden;
    opacity: 0;
    transition: transform 1s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.6s ease;
  }
  .rig.ready .chassis { transform: rotateX(13deg); opacity: 1; }
  .chassis svg { display: block; width: 100%; height: auto; overflow: visible; }


  /* ── Components ───────────────────────────────────────────────────────── */
  /* Two roles, and they do not coincide. A .unit names itself with a tag —
     that is a drive, a memory bank, a CPU, a network card. A .pick physically
     comes out — a DIMM, a drive, a fan, a heatsink. A memory bank is one
     .unit with sixteen .pick inside it: one tag per bank, and it is the DIMM
     that lifts. */
  .unit { cursor: pointer; }
  .rig.service .unit { cursor: grab; }
  .pick-body {
    transform-box: fill-box; transform-origin: center;
    transition: transform var(--glide);
  }
  /* Hover: the unit lifts a little and brightens. Eight points of lift read
     as though the part had already been pulled; three read as a response. */
  .pick:hover .pick-body { transform: translateY(-3px); filter: brightness(1.2) saturate(1.08); }
  .pick .pick-body { transition: transform var(--glide), filter var(--fade); }

  /* The tag lives next to its unit and does not run past the edge of the
     picture. There are deliberately no shadows on the SVG any more: in Safari
     a filter inside preserve-3d floods the stage white — that was the "white
     screen" in service mode. */
  .unit .tag { fill-opacity: 0; transition: fill-opacity var(--fade); pointer-events: none; }
  .unit:hover .tag, .unit:focus-visible .tag { fill-opacity: 1; }
  .unit:focus-visible { outline: none; }
  /* Callouts. A visitor has to see where to click without hovering — so the
     labels are permanent, and hover only highlights the one in question. */
  .callouts { pointer-events: none; }
  a.callout { pointer-events: auto; cursor: pointer; }
  a.callout:hover .co-box { fill: #fff; stroke: var(--cyan); stroke-width: 2.4; }
  a.callout:hover .co-line { stroke: var(--cyan); }
  a.callout:focus-visible { outline: none; }
  a.callout:focus-visible .co-box { stroke: var(--cyan); stroke-width: 2.2; }
  /* In service mode the callouts go away entirely: there the machine is taken
     apart, not read, and the labels stopped fighting the pulled units that
     travel straight through them.

     Уходят они плавно и все разом: лесенка на уходе читается задержкой, а не
     списком, — а возвращаются по-прежнему по одной, это правило .callout ниже.
     Но ссылкой плашка перестаёт быть сразу, в тот же кадр, и вот это трогать
     нельзя. Прозрачная <a> по-прежнему ловит и курсор, и Tab: opacity и
     pointer-events тут уже пробовали, и подписи открывали адреса, которых не
     видно.
     Поэтому visibility остаётся — но переезжает на саму <a>: она мгновенно
     уходит из-под курсора и из порядка обхода, а дети объявляют себе visible и
     потому продолжают рисоваться, пока идёт затухание. pointer-events на <a>
     нужен там же и отдельно: дети наследуют его от неё, и перебить её может
     только правило со специфичностью выше, чем `a.callout`. */
  .rig.service a.callout { visibility: hidden; pointer-events: none; }
  .rig.service a.callout > * { visibility: visible; }
  /* Линия и точка — цвета своего сервиса, того же, что полоска у кромки бирки
     и значок на ней. Цвет приходит переменной от генератора: он один на всю
     выноску, и менять его в двух местах значит однажды их развести. Подсветка
     ниже перебивает его нарочно — под курсором все выноски одинаково
     бирюзовые, чтобы было видно, какая из них поднята. */
  .co-dot { fill: var(--accent, var(--cyan)); stroke: rgba(9,32,38,0.85); stroke-width: 1.8; }
  .co-line { stroke: var(--accent, rgba(42,161,152,0.8)); stroke-width: 2.2; }
  /* The backing is not decoration: without it the light label merges with the
     heatsink nameplates and the power supply stickers, of which the board has
     plenty. */
  .co-shadow { fill: rgba(3, 14, 18, 0.55); }
  .co-box { fill: rgba(245, 241, 228, 0.98); stroke: rgba(6, 26, 31, 0.9); stroke-width: 1.6; }
  /* The colour of the spine arrives as an attribute from the generator — one
     per service. A fill rule here would override it: a presentation attribute
     is weaker than any style, and all seven labels would go identical
     again. */
  .co-text {
    fill: #06222a; font-family: ui-monospace, Menlo, monospace;
    font-size: 22px; font-weight: 700; letter-spacing: 0.01em;
  }
  /* The second line answers "and what is this": without it the label names
     the service but does not say why one would click it. */
  .co-sub {
    fill: rgba(6, 34, 42, 0.6); font-family: ui-monospace, Menlo, monospace;
    font-size: 12.5px; font-weight: 500; letter-spacing: 0.02em;
  }
  /* The service icon is the same one as in the card of links: a visitor sees
     the same mark in both views of the card and recognises it faster than he
     reads it. */
  .co-icon { fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
  /* The outward arrow is a web convention: the link opens in a new tab. */
  .co-ext path {
    stroke: rgba(6, 34, 42, 0.45); stroke-width: 1.9;
    stroke-linecap: round; stroke-linejoin: round;
  }
  /* The shine crosses the label exactly once, when the machine is assembled:
     it says "this can be clicked", and repeated it would turn into a blinking
     banner. */
  .co-shine { fill: rgba(255, 255, 255, 0.9); opacity: 0; }
  .rig:not(.assembly) .co-shine {
    animation: coShine 0.85s calc(1.15s + var(--tag-order, 0) * 0.14s) ease-out 1;
  }
  @keyframes coShine {
    from     { opacity: 0;    transform: translateX(0); }
    22%      { opacity: 0.5; }
    to       { opacity: 0;    transform: translateX(380px); }
  }

  .tag rect { fill: rgba(238,232,213,0.94); }
  .tag text { fill: #073642; font-family: ui-monospace, Menlo, monospace; font-size: 15px; font-weight: 600; }

  .decor { pointer-events: none; }
  .cables { opacity: 0.9; }


  /* ── term ──────────────────────────────── */
  /* Completion hint. An <input> cannot hold two colours at once, so a mirror
     lies under the field: the part already typed is transparent in it and is
     there only to take up the width, while the continuation shows in a muted
     tone. The metrics have to match the field to the pixel, otherwise the hint
     drifts out of line with the text. */
  .prompt-field { position: relative; flex: 1; min-width: 0; display: block; }
  .prompt-field input { position: relative; z-index: 1; background: transparent; width: 100%; }
  .ghost {
    position: absolute; inset: 0; pointer-events: none; overflow: hidden;
    white-space: pre; display: block;
    font: inherit; letter-spacing: inherit;
    line-height: normal;
  }
  .ghost-typed { color: transparent; font-style: normal; }
  .ghost-rest { color: var(--dim); opacity: 0.7; font-style: normal; }

  /* ── Indication ───────────────────────────────────────────────────────── */
  /* We put lamps out through fill-opacity, not through opacity. opacity on an
     SVG node lifts it into a separate composited layer, and those layers
     covered the whole stage — that is what "the entire background went black"
     looked like. fill-opacity changes the fill only and creates no layers. */

  .led { fill-opacity: 0.16; transition: fill-opacity 0.2s steps(1, end); }
  .rig.on .led { fill-opacity: 1; }

  /* Standby: the BMC, the management port and the power supplies themselves
     run always — the machine is off, and they are already live. */
  .led.aux { fill-opacity: 1; }
  .led-act.aux { animation: act var(--blink) steps(1, end) infinite; }
  .fan-blades.aux { animation-play-state: running; }

  /* The blink pattern. Every switching point is a multiple of 5% of the
     period, and the period is a multiple of a second — so any change falls on
     the common tick of a tenth of a second. The numbers here are not
     cosmetic: shift one by a per cent and the lamp starts repainting the
     stage on frames of its own. */
  @keyframes act {
    0.00% { fill-opacity: 1 }
    3.57% { fill-opacity: 1 }
    7.14% { fill-opacity: 1 }
    10.71% { fill-opacity: 0.1 }
    14.29% { fill-opacity: 0.1 }
    17.86% { fill-opacity: 0.1 }
    21.43% { fill-opacity: 0.1 }
    25.00% { fill-opacity: 1 }
    28.57% { fill-opacity: 1 }
    32.14% { fill-opacity: 0.1 }
    35.71% { fill-opacity: 0.1 }
    39.29% { fill-opacity: 1 }
    42.86% { fill-opacity: 1 }
    46.43% { fill-opacity: 1 }
    50.00% { fill-opacity: 1 }
    53.57% { fill-opacity: 1 }
    57.14% { fill-opacity: 0.1 }
    60.71% { fill-opacity: 0.1 }
    64.29% { fill-opacity: 1 }
    67.86% { fill-opacity: 1 }
    71.43% { fill-opacity: 0.1 }
    75.00% { fill-opacity: 0.1 }
    78.57% { fill-opacity: 0.1 }
    82.14% { fill-opacity: 1 }
    85.71% { fill-opacity: 1 }
    89.29% { fill-opacity: 0.1 }
    92.86% { fill-opacity: 0.1 }
    96.43% { fill-opacity: 1 }
    100% { fill-opacity: 1 }
  }
  /* The phase arrives inline from the generator, the period is shared. The
     halo snaps together with the core rather than crawling across smoothly: a
     smooth halo means a repaint on every frame of the monitor, and forty
     lamps at once left the stage no idle time. */
  .rig.on .led-act { animation: act var(--blink) steps(1, end) infinite; }
  /* Приём и передача — одна лампа на два цвета, как на живой розетке: в ней
     два кристалла под общей линзой, и куда ушёл пакет, видно по тому, каким
     цветом она моргнула. Янтарный — наружу, зелёный — внутрь. Двумя лампами
     это говорилось дважды.
     Мигает по-прежнему act, здесь меняется только заливка, поэтому вторая
     анимация идёт рядом, а не вместо. Период втрое длиннее вспышек, чтобы
     цвет держался несколько миганий подряд: смена цвета на каждой вспышке
     читается мельтешением, а не направлением. Кратно 0.05 с, как всё
     остальное в индикации. */
  .rig.on .led-act.led-txrx {
    animation:
      act var(--blink) steps(1, end) infinite,
      txrx calc(var(--blink) * 3) steps(1, end) infinite;
  }
  @keyframes txrx { 0%, 50% { fill: var(--amber) } 50.01%, 100% { fill: var(--green) } }
  .halo { transition: fill-opacity 0.35s ease; }
  /* Ореол не мигает: он и так виден, а анимаций в сцене от него втрое
     больше, чем ламп, — по три круга на каждую. Браузер обновляет каждую
     анимацию в каждом кадре, и эта плата не зависит от того, изменилось
     ли что-нибудь на экране. */
  .rig.on .led-act.halo { animation: none; fill-opacity: 0.75; }
  .led-hb.halo, .fault.halo, .fault-sys.halo, .led-id.halo { transition: fill-opacity 0.4s ease; }
  .rig:not(.init) .led-hb.halo { animation: none; fill-opacity: 0.6; }
  .pick.pulled .fault.halo, .rig.has-fault .fault-sys.halo { animation: faultblink var(--beat) steps(1, end) infinite; }
  .rig.identify .led-id.halo { animation: idblink var(--beat) steps(1, end) infinite; }

  /* Линк сети поднимается первым — раньше, чем стартует хост. */
  .led-link { fill-opacity: 0.12; transition: fill-opacity 0.15s steps(1, end); }
  .rig.net .led-link, .led-link.aux { fill-opacity: 1; }

  /* Порт, к которому ничего не подключено: лампа активности стоит тёмной и
     не мигает — рядом с ней янтарный линк, и вместе они читаются как связь
     на пониженной скорости. */
  .led-idle { fill-opacity: 0.3; }

  /* Heartbeat BMC: такой же резкий, но единственный строго ровный —
     короткая вспышка раз в секунду, всё остальное дёргается вразнобой. */
  .led-hb { fill-opacity: 0.1; }
  .rig:not(.init) .led-hb { animation: hb var(--beat) steps(1, end) infinite; }
  @keyframes hb { 0%, 10% { fill-opacity: 1 } 15%, 100% { fill-opacity: 0.1 } }

  /* Неисправность. .fault — лампа своего узла и лежит внутри него, поэтому
     загорается ровно у того, что вынули. .fault-sys — общая, спереди и
     сзади, и зажигается от любой ошибки. */
  circle.fault, circle.fault-sys { fill-opacity: 0; transition: fill-opacity 0.15s steps(1, end); }
  .pick.pulled > .fault, .pick.pulled .fault { fill-opacity: 1; animation: faultblink var(--beat) steps(1, end) infinite; }
  .rig.has-fault .fault-sys { fill-opacity: 1; animation: faultblink var(--beat) steps(1, end) infinite; }
  @keyframes faultblink { 0%, 50% { fill-opacity: 1 } 55%, 100% { fill-opacity: 0.05 } }




  /* ── lightpath ──────────────────────────────── */
  /* Light Path Diagnostics: the panel slides forward, past the chassis outline. */
  .lightpath {
    transform: translateX(194px);
    visibility: hidden;
    fill-opacity: 0; stroke-opacity: 0;
    transition: transform var(--spring), fill-opacity var(--fade), stroke-opacity var(--fade);
    pointer-events: none;
  }
  /* The pulled-out panel butts against its own front face instead of hanging
     off to the side of it: 22 is exactly the gap between the right edge of the
     panel drawing and the left edge of the tab. */
  .rig.lp-open .lightpath {
    transform: translateX(22px);
    visibility: visible;
    fill-opacity: 1; stroke-opacity: 1;
  }
  .lp { fill-opacity: 0.07; transition: fill-opacity 0.15s steps(1, end); }
  /* A full-set panel: sixteen lamps, as on a live machine. The units the
     machine can diagnose light their own; the network card lights LINK, the
     riser lights PCI. The remaining lamps exist, but in this build there is
     nothing to light them — same as on a real panel. */
  .rig.fault-mem .lp-mem, .rig.fault-cpu .lp-cpu, .rig.fault-fan .lp-fan,
  .rig.fault-nic .lp-link, .rig.fault-rsr .lp-pci, .rig.fault-ps .lp-ps,
  .rig.has-fault .lp-log, .rig.has-fault .lp-brd { fill-opacity: 1; }
  .lp-tab { cursor: pointer; }
  /* Highlight the body of the tab, not everything standing on it:
     the lamps are rectangles too, and a general selector outlined them. */
  .lp-tab:hover > rect { stroke: rgba(147,161,161,0.6); }

  .silk { opacity: 0.85; }

  /* ── service ──────────────────────────────── */
  .svc-switch { cursor: pointer; }
  .svc-knob { transition: transform var(--spring), fill var(--fade), stroke var(--fade); transform-box: fill-box; }
  .rig.service .svc-knob { transform: translateX(52px); fill: rgba(181,137,0,0.35); stroke: rgba(181,137,0,0.7); }
  .svc-switch:hover .svc-knob { stroke: rgba(147,161,161,0.7); }

  /* Блик по выключенному тумблеру. Погашенный он — тёмный прямоугольник на
     тёмной плате, и единственное, что на схеме вообще можно переключить, гость
     находил не сразу. Полоса проходит по нему раз в несколько секунд: коротко
     и не в полную силу, чтобы это читалось отсветом на пластике, а не мигающей
     рекламой.

     Повторяется — в отличие от блика на бирках-ссылок, который проходит один
     раз при сборке. Там он представляет узел, здесь зовёт нажать, и замолчать
     ему можно только после того, как нажали: в сервисном режиме блика нет. */
  .svc-shine { fill: rgba(238,232,213,0.9); opacity: 0; }
  .rig:not(.service):not(.assembly) .svc-shine {
    animation: svcShine 3.4s ease-in-out infinite;
  }
  @keyframes svcShine {
    0%, 64%  { opacity: 0;    transform: translateX(-26px); }
    72%      { opacity: 0.24; }
    100%     { opacity: 0;    transform: translateX(140px); }
  }

  /* ── lid ──────────────────────────────── */
  /* Cover. Lives in the same coordinates as the board, so it sits over the
     machine exactly. A person takes it off with the cover button; it does not
     intercept clicks. */
  .lid {
    position: absolute; inset: 0;
    pointer-events: none;
    z-index: 3;
    transition: transform 0.9s cubic-bezier(0.32,0.72,0,1), opacity 0.7s ease;
    z-index: 3;
  }
  .lid svg { display: block; width: 100%; height: auto; }
  .lid-btn-svg { pointer-events: auto; cursor: pointer; }
  .lid-btn-svg:hover rect:first-child { stroke: var(--cyan); fill: #22303a; }
  .rig.lid-off .lid { transform: translateY(-30%) scale(1.03); opacity: 0; }

  .id-btn, .lid-on-btn { cursor: pointer; }
  .lid-on-btn { display: none; }
  .rig.lid-off .lid-on-btn { display: block; }
  .lid-on-btn:hover rect:nth-of-type(2) { stroke: var(--cyan); fill: #1c2a31; }
  .id-btn:hover circle:first-child { stroke: rgba(181,137,0,0.6); }

  /* ── Сборка машины ─────────────────────────────────────────────────────
   * Первый заход показывает пустое шасси и то, как в него ставят узлы.
   * Расписание — в geom.py, оно приезжает сюда переменной --seat на каждом
   * узле; как именно узел садится, описано в его собственном css.
   *
   * Класс стоит прямо в разметке: скрипт подключён с defer и мог не успеть
   * до первого кадра. Скрипт его только снимает — и сразу, если машину уже
   * видели или гость просил не анимировать.
   */
  /* Бирки проступают после сборки, а не включаются вместе с ней. Переход
     объявлен на самих подписях, а не внутри .rig.assembly: правило с классом
     исчезает вместе с классом, и гасить-то оно гасило, а проявлять было уже
     нечему — подписи возникали рывком. */
  /* Бирки проступают одна за другой: глаз проходит по ним списком, а не
     встречает семь плашек разом. Порядок каждая знает сама — генератор
     ставит ей --tag-order. */
  .callout {
    opacity: 1;
    transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1)
                calc(0.35s + var(--tag-order, 0) * 0.14s);
  }
  .rig.assembly .callout { opacity: 0; transition: opacity 0.2s linear; }
  /* И ждут экрана. Собранная машина включается, поднимается самотест — подписи
     проступают только когда экран уехал. Иначе порядок был бессмысленный:
     плашки всплывали на секунду и тут же прятались под экраном, а после него
     появлялись во второй раз. */
  .rig.tags-off .callout { opacity: 0; transition: opacity 0.25s linear; }
  /* Уход в сервисный режим — одно общее движение: все подписи гаснут разом и
     за полсекунды. Возвращаются они той же лесенкой, что и после сборки:
     класс service снимается, и работает переход выше, со своей задержкой на
     каждую. Разница нарочная — уходя, они освобождают место, и растягивать
     это на полторы секунды незачем; возвращаясь, читаются списком. */
  .rig.service .callout { opacity: 0; transition: opacity 0.45s ease; }
  /* Пока идёт сборка, разбирать нечего: клики по полусобранной машине
     оставляли бы узлы в состоянии, из которого их никто не вернёт. */
  .rig.assembly .pick, .rig.assembly .unit { pointer-events: none; }

  /* Схема ушла с экрана или вкладку свернули — крутить нечего. Пауза, а не
     отмена: анимации продолжатся с того же места, включая недоигранную
     сборку. */
  .rig.dormant *, .rig.dormant *::before, .rig.dormant *::after {
    animation-play-state: paused !important;
  }

  /* ── Сервисный режим: извлечение поштучно ─────────────────────────────── */
  .rig.service .pick { cursor: grab; }
  .rig.service .pick.pulled .pick-body { transform: translateY(-24px); }
  /* Диск выезжает вперёд, к фронту — то есть влево. */
  /* ── drives ──────────────────────────────── */
  /* A drive comes out in two moves: first the handle is unlatched, and only
     then does the caddy travel out. The handle is a lever the full length of
     the caddy, hinged on an axis at the far edge: it swings outward, towards
     the front, rather than sliding. transform-box: fill-box is needed so that
     the percentage in transform-origin counts from the group itself and not
     from the canvas.

     The handle now sits inside .pick-body rather than beside it, and that one
     move fixes two faults: hovering used to lift the body and leave the handle
     behind, and pulling drove the two by separate rules so they travelled
     apart. The whole part moves as one now, and the handle's only job of its
     own is to swing open. */
  .bay-handle {
    transform-box: fill-box;
    transform-origin: 100% 100%;
    /* The caddy latch clicks: it holds until you overpower it, and then it
       throws itself open. The caddy itself travels evenly — nothing holds it
       any more. */
    transition: transform var(--click);
  }

  /* The travel is small on purpose. A lever thrown open to its full swing lies
     across the neighbouring bay, and the frame then reads not as "the drive is
     unlatched" but as "something fell off". Moving left with a slight tilt says
     the same thing and gets in nobody's way: the handle has left the latch, and
     the drive itself is exposed under it. */
  .rig.service .bay.unlatched .bay-handle,
  .rig.service .blank.unlatched .bay-handle { transform: translateX(-16px) rotate(-7deg); }

  /* Once the caddy is out, the handle stops having a life of its own: it is
     one part in a hand now, not a lever on a machine. Leaving it thrown open
     was what made a pulled drive read as two things flying apart — the frame
     lagging behind and below its own head. */
  .rig.service .bay.pulled .bay-handle,
  .rig.service .blank.pulled .bay-handle { transform: none; }

  /* Outward, past the chassis outline, and far enough to clear the cage. The
     front frame is a sliver; what is worth seeing is the drive that was hidden
     behind it, so the travel is the depth of the drive plus its frame and the
     whole part ends up outside the machine instead of half inside it. */
  .rig.service .bay.pulled .pick-body,
  .rig.service .blank.pulled .pick-body { transform: translateX(-152px); }

  /* The drive itself sits deep in the chassis and cannot be seen until the
     caddy comes out — in an assembled machine only the front frame shows. It
     fades in rather than appearing, so the frame reads as a part being drawn
     out of the case and not as a rectangle switching on.

     Both opacities have to go, not just the fill: fill-opacity leaves the
     outline alone, and an invisible drive still drew its own edges straight
     through the cage grille — the one thing that gave away that something was
     hiding in there. */
  .drive-body {
    fill-opacity: 0;
    stroke-opacity: 0;
    transition: fill-opacity var(--fade), stroke-opacity var(--fade);
  }
  .rig.service .bay.pulled .drive-body,
  .rig.service .blank.pulled .drive-body { fill-opacity: 1; stroke-opacity: 1; }

  /* Assembly: the drive is pushed in from the front, along the same path it
     takes coming out during teardown — only the other way round. */
  .rig.assembly .bay,
  .rig.assembly .blank {
    animation: seatBay var(--press-seat) var(--seat, 0s) backwards;
  }
  /* The caddy runs along its rails freely and meets resistance only right at
     the end: the last centimetre is the handle pressing it home to the click. */
  @keyframes seatBay {
    from { opacity: 0; transform: translateX(-152px); }
  }
  /* ── fans ──────────────────────────────── */
  /* Rotation went back to smooth, and the steps are gone.
   *
   * They were there for a real reason: animation in a large SVG is not
   * composited, so a turn made the browser rasterise the area and reassemble
   * the scene on every frame the monitor drew. Twenty steps a second instead
   * of a hundred and forty-four was worth the jerk.
   *
   * will-change changes the deal. It lifts each rotor into a layer of its own
   * — checked, not assumed: with it the layer tree gains exactly sixteen
   * layers the size of an impeller, and without it none. A transform on a
   * layer is the compositor's job and does not touch the main thread, so
   * smooth costs what stepped used to and looks like nothing at all.
   *
   * The grid the lamps blink on does not apply here any more, and with it goes
   * the rule that a period had to divide into whole seconds. */
  .fan-blades {
    will-change: transform;
    animation: spin var(--spin) linear infinite;
    animation-play-state: paused;
  }
  .fan-blades.aux { animation-duration: var(--spin-aux); }
  .rig.on .fan-blades { animation-play-state: running; }

  /* Blades or blur. Past a few thousand rpm an impeller is a translucent disc
     and nothing else — a blade crosses any given point faster than the eye
     takes it in. So a running wall shows the disc, and that is the default
     state here, not the exception.

     The blades come back exactly where they would in life: on a machine that
     is off, on a fan that has been pulled, and when the supply profile drops
     the rpm. Between the two states we cross-fade, so spinning up reads as the
     blades smearing rather than as one picture being swapped for another.

     Through fill-opacity and not opacity: opacity on an SVG node lifts it into
     a layer of its own, and the rotor already has one. */
  .rotor-vane { fill-opacity: 0.05; transition: fill-opacity var(--fade); }
  .rotor-blur { fill-opacity: 1; transition: fill-opacity var(--fade); }

  .rig:not(.on) .rotor-vane,
  .rig.nv-eff .rotor-vane,
  .fan.pulled .rotor-vane,
  .fan-blades.aux .rotor-vane { fill-opacity: 0.42; }

  .rig:not(.on) .rotor-blur,
  .rig.nv-eff .rotor-blur,
  .fan.pulled .rotor-blur,
  .fan-blades.aux .rotor-blur { fill-opacity: 0; }

  /* Pull even one fan and the rest go to maximum. Any machine behaves this
     way: the controller loses one airflow and makes up for it with rpm. The
     generator writes every impeller its own period inline, which is why
     !important is needed here. The power supply fan lives its own life — it
     has a controller of its own and does not spin up. */
  .rig.on:has(.fan.pulled) .fan-blades:not(.aux) { animation-duration: 0.28s !important; }
  .rig.on .fan.pulled .fan-blades { animation-play-state: paused; }
  .fan.pulled .fan-blades { animation-play-state: paused; }
  @keyframes spin { to { transform: rotate(360deg); } }

  .rig.service .fan.pulled .pick-body { transform: translateY(-30px) scale(1.03); }

  /* Assembly: the fan is lowered into the wall from above — in 1U it goes in
     vertically, between the cage upright and the board there is no room for any
     other movement. The moment of seating is set by --seat, which the generator
     writes: the schedule for the whole assembly lives in geom, and only "from
     where and how" lives here. */
  .rig.assembly .fan {
    animation: seatFan var(--press-hard) var(--seat, 0s) backwards;
  }
  /* It seats stiffly: the guides are long, the module travels along them under
     effort, and right at the bottom it is pressed home to the latch. */
  @keyframes seatFan {
    from { opacity: 0; transform: translateY(-42px); }
  }

  /* A latch holds the fan in the wall: you pull, it does not move, then it
     breaks free. Exactly the same curve carries it when seating too. */
  .fan .pick-body { transition: transform var(--click); }
  /* ── memory ──────────────────────────────── */
  /* On a live board the latches are milky-white translucent plastic, and there
     are two of them: one at each end. The error lamp sits right inside the
     latch — "DIMM error LEDs are located in the handles", as the diagram of the
     live machine puts it — so on a fault it is the latch that turns amber. */
  /* The edge of the latch is blue: in the replacement colour code that means
     "power down first". A DIMM is not swapped hot, and the colour says so
     before the visitor ever gets to the label on the cover. */
  .latch {
    fill: rgba(226, 235, 231, 0.42);
    stroke: rgba(58, 143, 196, 0.85);
    stroke-width: 1.1;
    transform-box: fill-box;
    transition: fill 0.15s steps(1, end), transform var(--spring);
  }
  /* The hinge sits at the outboard end of the socket, not next to the module:
     a latch tips away from the DIMM it holds. Hence the pivot is the far
     corner of each ear — left one on its left edge, right one on its right. */
  .latch-l { transform-origin: 0% 100%; }
  .latch-r { transform-origin: 100% 100%; }
  .dimm.pulled .latch { fill: var(--amber); }

  /* The DIMM cannot come out while the latches are closed — so they throw open
     first, and only then does the module travel up. The socket meanwhile stays
     on the board: it is drawn separately and goes nowhere. */
  /* Open means outward: the board runs front-to-right, so the front ear swings
     toward -X and the rear one toward +X — both away from the socket, never
     across the module. A positive angle is clockwise in SVG. */
  .rig.service .dimm.pulled .latch-l { transform: rotate(-38deg); }
  .rig.service .dimm.pulled .latch-r { transform: rotate(38deg); }
  .rig.service .dimm.pulled .pick-body {
    transform: translateY(-13px);
    transition-delay: 0.12s;
  }

  /* Assembly: the DIMM enters its socket with a short downward move — its
     travel is small, and the swing has to be as small. The order is set by
     --seat: banks are filled by channel rather than one after another, and the
     halves of the middle bank go to different processors. */
  .rig.assembly .dimm .pick-body {
    animation: seatDimm var(--press-hard) var(--seat, 0s) backwards;
  }
  /* The latches are part of the board, not of the DIMM: they are riveted to
     the socket and always stay put. While there is no module they are open:
     that is exactly how an empty slot on a live machine looks, with its ears
     splayed aside. They close at the moment the module is pressed home — hence
     the same --seat delay. */
  .rig.assembly .latch-l { animation: latchL var(--press-hard) var(--seat, 0s) backwards; }
  .rig.assembly .latch-r { animation: latchR var(--press-hard) var(--seat, 0s) backwards; }
  @keyframes latchL { from { transform: rotate(-38deg); } }
  @keyframes latchR { from { transform: rotate(38deg); } }
  /* A DIMM is not lowered but pressed in: it goes against resistance and
     seats when the latches at its ends click. */
  @keyframes seatDimm {
    from { opacity: 0; transform: translateY(-22px); }
  }

  /* The DIMM is held by the latches at its ends: until they throw open the
     module stands dead solid, and after that it comes out with a jerk. */
  .dimm .pick-body { transition: transform var(--click); }
  /* ── risers ──────────────────────────────── */
  /* The riser and the power supply come out backwards, through the rear wall. */
  .rig.service .riser.pulled .pick-body { transform: translateX(46px); }

  /* Assembly: the riser is lowered into its slot from above, whole — together
     with the card and the bracket. */
  .rig.assembly .riser {
    animation: seatRiser var(--press-hard) var(--seat, 0s) backwards;
  }
  /* The riser seats along its whole connector at once, so it goes in stiff and
     needs a final push — that is exactly what the tab on it is held by. */
  @keyframes seatRiser {
    from { opacity: 0; transform: translateY(-56px); }
  }

  /* The riser sits tight in its slot: it breaks free whole, card and all. */
  .riser .pick-body { transition: transform var(--click); }
  /* ── psu ──────────────────────────────── */
  .rig.service .psu.pulled .pick-body { transform: translateX(74px); }

  /* A pulled supply is dead: mains no longer reaches it. Both lamps go out —
     AC included, the one that burns even on a switched-off machine — and the
     fan stops. The bay lamp, though, sits on the board and stays where it is:
     what lights up is not the supply but the socket it was pulled out of. */
  .psu.pulled .led, .psu.pulled .led.aux,
  .psu.pulled .fault-sys, .psu.pulled .fault-sys.halo { fill-opacity: 0.1; animation: none; }
  .psu.pulled .fan-blades { animation-play-state: paused; }

  /* Assembly: the power supply goes in from the back, out of its own bay. It
     travels further than the rest — its path to the connector is long, and the
     hand can tell. */
  .rig.assembly .psu {
    animation: seatPsu var(--press-snap) var(--seat, 0s) backwards;
  }
  /* The supply flies into the bay in one movement — the guides are long and
     straight, and only the connector at the end slows it down. */
  @keyframes seatPsu {
    from { opacity: 0; transform: translateX(96px); }
  }

  /* The power supply is held by the orange latch — that very click. */
  .psu .pick-body { transition: transform var(--click); }

  /* ── Оба блока вынуты: машина обесточена ───────────────────────────────
     Резервирование кончилось — кончилось и питание. Гаснет всё разом,
     включая дежурку: BMC, порт управления и лампы AC висят на том же вводе.
     Это единственное состояние схемы, где не мигает вообще ничего.

     Гаснет свет, а не лампа: fill-opacity обнуляется только у цветного ядра
     и ореола, а гнездо нарисовано без класса и остаётся на месте. Обесточенная
     машина показывает, где стоит индикация, — как и живая. */
  .rig.blackout .led, .rig.blackout .led-link, .rig.blackout .led-hb,
  .rig.blackout .led-id, .rig.blackout .fault, .rig.blackout .fault-sys,
  .rig.blackout .lp, .rig.blackout .seg {
    fill-opacity: 0 !important;
    animation: none !important;
  }
  .rig.blackout .fan-blades { animation-play-state: paused !important; }
  /* Кнопка питания — тоже лампа, и гаснет по той же причине. !important
     здесь ровно затем же, зачем ниже у консоли: правила кнопки живут в
     блоке фронта, а он встаёт в общий css после этого. */
  .rig.blackout .pwr-led { stroke-opacity: 0 !important; animation: none !important; }

  /* Консоль — это SOL к BMC, а BMC питается от той же дежурки: машина
     обесточена, значит и консоль мертва. Серая и не отвечает — ввод при этом
     ещё и запрещён в скрипте, потому что на pointer-events клавиатура не
     смотрит.
     opacity через !important: кусок блока встаёт в общий css выше правил
     сервисного режима, а там у .rig-side своя прозрачность с той же
     специфичностью — спор решил бы порядок, и консоль осталась бы яркой. */
  .rig.blackout .rig-side {
    filter: grayscale(0.85);
    opacity: 0.42 !important;
    pointer-events: none;
  }

  /* ── cpu ──────────────────────────────── */
  /* The processor stays in the socket — what comes off is the heatsink, as if
     by hand. The die is uncovered under it: the same gradient runs over it as
     over the "download" button on the CV. While the heatsink is in place the
     sheen is off — it would spin an animation under an opaque cover for
     nothing. */
  /* The die and the contact field do not switch off with a jerk: first they
     show through, and they are hidden only after they have faded out. Hence
     the zero-duration visibility with a delay — it waits out the fade. */
  .die, .lga {
    visibility: hidden; opacity: 0;
    transition: opacity var(--fade), transform var(--glide), visibility 0s linear 0.45s;
  }
  .rig.service .cpu-slot.pulled .die,
  .rig.service .cpu-slot.opened .lga {
    visibility: visible; opacity: 1;
    transition: opacity var(--fade), transform var(--glide), visibility 0s;
  }
  /* The sheen goes diagonally, from the bottom-left corner to the top-right —
     straight across it read as a band of glare, not as play on polished
     silicon. */
  /* The sheen over the silicon is the most expensive animation on the
     schematic: a big gradient band under a clip-path, and it used to repaint on
     every frame of the monitor. Eighty steps over eight seconds give the same
     ten repaints a second as the lamps, and fall into the common beat. */
  .die-shine { animation: dieShine 8s steps(80, end) infinite; }
  @keyframes dieShine {
    0%, 100% { transform: translate(0, 0); }
    50%      { transform: translate(150px, -82px); }
  }

  /* Taking the processor apart goes in two moves, as in life: first the
     heatsink comes off, and only then, from under it, the processor itself.
     The heatsink travels further than it used to: it is the size of the
     socket, and at the old 104 pixels it covered the place where the processor
     has to lie. */
  .heatsink, .cpu-lid { transition: transform var(--glide); }
  .rig.service .cpu-slot.pulled .pick-body { transform: none; }
  .rig.service .cpu-slot.pulled .heatsink { transform: translate(0, -164px) scale(0.92); }
  .rig.service .cpu-slot.opened .cpu-lid { transform: translate(0, 116px) scale(0.96); }

  /* The contact field: while the processor is in place it is not visible — and
     rightly so, there is nothing to show under it. It shows through together
     with the die, see above. */
  /* The die lives on the processor and travels away with it: a removed
     processor glowing on an empty socket is no longer a teardown but a
     magic trick. */
  .rig.service .cpu-slot.opened .die { transform: translate(0, 116px) scale(0.96); }

  /* Assembly: the processor with its heatsink is lowered into the socket from
     above and turned slightly into place — that is how it is fitted, aiming by
     the keys. */
  /* Assembling the processor goes in two moves, as in life: first the
     processor itself settles into the socket and the sheen has time to run
     across the exposed die — and only then is the heatsink lowered on top.
     Seating them at once hid the die: it was covered at exactly the moment it
     appeared. */
  .rig.assembly .cpu-slot .cpu-lid {
    animation: seatCpu 0.7s var(--seat, 0s) cubic-bezier(0.22, 1, 0.36, 1) backwards;
  }
  .rig.assembly .cpu-slot .heatsink {
    animation: seatCpu 0.7s calc(var(--seat, 0s) + 0.85s) cubic-bezier(0.22, 1, 0.36, 1) backwards;
  }
  @keyframes seatCpu {
    from { opacity: 0; transform: translateY(-52px) scale(0.94); }
  }
  /* The die travels along with its own processor instead of showing through in
     place: it lies on it. The first forty percent of the time is the same
     movement as the processor lid's, after that it simply stays exposed until
     the heatsink comes down on top. We hide it at the end not for looks — under
     the heatsink it cannot be seen anyway — but so the sheen does not keep
     spinning under an opaque cover. */
  .rig.assembly .cpu-slot .die {
    animation: dieSeat 1.75s var(--seat, 0s) cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  @keyframes dieSeat {
    from { visibility: visible; opacity: 0; transform: translateY(-52px) scale(0.94); }
    14%  { visibility: visible; opacity: 0.75; }
    40%  { visibility: visible; opacity: 1; transform: none; }
    72%  { visibility: visible; opacity: 1; transform: none; }
    to   { visibility: hidden; opacity: 0; transform: none; }
  }
  /* An empty socket is a contact field: while there is no processor it is
     exposed, and it goes out exactly when it gets covered. both holds both
     ends: visible before the start, not after. */
  .rig.assembly .cpu-slot .lga {
    animation: lgaCover 0.45s var(--seat, 0s) linear both;
  }
  @keyframes lgaCover {
    from { visibility: visible; opacity: 1; }
    to   { visibility: hidden; opacity: 0; }
  }

  /* ── front_panel ──────────────────────────────── */
  /* The power button is a ring, so it has a stroke and not a fill. It blinks
     fast while the BMC is coming up and must not be pressed; slowly — standby,
     you may switch on; steady — the machine is running. */
  .pwr-led { stroke-opacity: 0; }
  .rig.init .pwr-led { stroke-opacity: 1; animation: pwrblink 0.4s steps(1, end) infinite; }
  .rig.standby .pwr-led { stroke-opacity: 1; animation: pwrblink 2s steps(1, end) infinite; }
  .rig.on .pwr-led { stroke-opacity: 1; animation: none; }
  @keyframes pwrblink { 0%, 49% { stroke-opacity: 1 } 50%, 100% { stroke-opacity: 0.08 } }
  .rig.init .power-btn { cursor: not-allowed; }

  /* Identify in the rack: an even, hard blink so you can find the machine by eye. */
  .led-id { fill-opacity: 0; }
  .rig.identify .led-id { animation: idblink var(--beat) steps(1, end) infinite; }
  @keyframes idblink { 0%, 49% { fill-opacity: 1 } 50%, 100% { fill-opacity: 0 } }

  .power-btn { cursor: pointer; }
  .power-btn circle { transition: stroke var(--fade); }
  .power-btn:hover .pwr-ring { stroke: var(--green); }

  /* Square lamps on the front face of the diagnostics panel. The base rule
     puts out the round ones only — its selector goes by element — so these
     are put out here. They are lit by the same pair of classes as the round
     ones: that pair has higher specificity, so there is nothing to argue
     about. */
  .sq-led { fill-opacity: 0; transition: fill-opacity 0.15s steps(1, end); }

  /* Выход из сервиса: машина складывается одним движением, а не семью
     щелчками разом. Щелчок — это как узел ставят руками: треть времени он
     стоит, потом срывается. Когда так возвращаются все узлы сразу, читается
     не сборка, а рывок, и рядом с ним плавно едущая разметка выглядит
     отдельным механизмом.
     Поэтому на время возврата узлы идут по кривой всей композиции — по той
     же, по которой съезжается колонка с консолью. Правило стоит после всех
     блоков нарочно: у каждого свой переход с той же специфичностью, и спор
     решает порядок. */
  .rig.stowing .pick-body,
  .rig.stowing .bay-handle,
  .rig.stowing .latch,
  .rig.stowing .heatsink { transition: transform var(--svc); }

  .hint {
    margin-top: 14px; text-align: center;
    font-family: ui-monospace, Menlo, monospace; font-size: 0.68rem; color: var(--dim);
  }
  .hint b { color: var(--ink); font-weight: 600; }

  /* ── Мобильные ────────────────────────────────────────────────────────── */
  .fallback { display: none; flex-direction: column; gap: 8px; }


  @media (max-width: 820px) {
    .layout, .stage, .hint { display: none; }
    .fallback { display: flex; }
  }
  @media (prefers-reduced-motion: reduce) {
    .rig *, .rig *::before, .rig *::after { animation: none !important; transition-duration: 0.01ms !important; }
    .chassis { transform: rotateX(14deg); opacity: 1; }
    .lid { display: none; }
  }

/* ── Сервисный режим: консоль сверху, приборы справа ──────────────────────
 * Консоль над машиной — терминал, который подкатили к стойке. Приборы сбоку,
 * зеркально панели диагностики: light path выходит из корпуса слева, метрики
 * стоят справа.
 *
 * Сцена всегда ровно по центру и своего размера не меняет: боковые колонки
 * одинаковые и сжимаются сами. Иначе машина прыгала бы вбок при каждом
 * включении сервисного режима.
 */
.rig-body {
  display: grid;
  /* Вторая колонка есть всегда, просто в обычном виде она нулевой ширины:
     так её появление можно анимировать, а не включать display. */
  grid-template-columns: minmax(0, 1fr) 0px;
  align-items: start;
  /* Зазор шире обычного: перспектива расширяет схему за габарит сцены
     примерно на полсотни точек, и без запаса задняя панель подлезает
     под приборы. */
  column-gap: 0;
  row-gap: 0;
  /* Композиция целиком — машина и приборы — держится посередине экрана.
     Без потолка приборы прижимались к правому краю окна, а машина уезжала
     от центра тем дальше, чем шире монитор. */
  max-width: calc(var(--rig-w) + 700px);
  margin: 0 auto;
  /* Поля под перспективу: наклон уводит корпус за габарит сцены примерно на
     тридцать точек в каждую сторону, и без запаса машина упирается в кромку
     окна, хотя по разметке стоит ровно. */
  padding: 0 26px;
  transition:
    grid-template-columns var(--svc),
    column-gap var(--svc),
    row-gap var(--svc);
}
/* Сервисный режим открывает вторую колонку. Машина при этом занимает всё,
   что осталось, и плавно уходит влево — ровно на половину появившейся
   колонки. */
/* Консоли нужно место, и она его получает за счёт машины. Схема при этом не
   ужимается — ужатая, она перестаёт читаться, — а уезжает влево и краем
   уходит за кромку окна. В стойке к работающей машине тоже подходят сбоку,
   так что вид сходится с делом: смотришь в консоль, а не на шасси. */
.rig.service .rig-body {
  grid-template-columns: minmax(0, 1fr) clamp(380px, 32vw, 640px);
  column-gap: 24px; row-gap: 14px;
}
/* Машина занимает колонку целиком и ужимается вместе с ней. Уводить её за
   левую кромку окна нельзя: диск в сервисном режиме выезжает из корзины
   влево, и всё, что машина отдала за край, отнимается у него — вынуть диск
   становится нечем.
   Полей тут больше нет, и это не уборка. Стояло margin-left: 0, и
   переключение начиналось рывком: auto-поля не интерполируются, машина
   мгновенно перепрыгивала к левому краю и только потом колонка начинала
   ехать. Теперь меняется одна величина — ширина колонки, — а схема идёт за
   ней сама, оставаясь в своей колонке по центру. */
.rig.service .stage {
  width: min(100%, var(--rig-w));
}

/* Места в гриде заданы явно. Без этого лента, объявившая себе первую
   колонку, уводила приборы во вторую строку — и в сервисном режиме они
   оказывались под машиной справа, а страница обзаводилась прокруткой. */
.stage    { grid-area: 1 / 1 / 2 / 2; }
.rig-side { grid-area: 1 / 2 / 3 / 3; }
.timeline { grid-area: 2 / 1 / 3 / 2; }

/* 96%, а не 100%: перспектива уводит нижний край схемы за габарит сцены,
   и последние проценты — это запас, чтобы задняя панель не подлезала
   под приборы. */
.stage { width: min(96%, var(--rig-w)); margin: 0 auto; }

.rig-side {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 14px;
  height: 100%;
  /* Колонка нулевой ширины: приборы из неё не должны торчать, пока их не
     позвали. Появляются они не морганием display, а выездом справа. */
  overflow: hidden;
  opacity: 0;
  transform: translateX(28px);
  /* Приборы идут в такт с колонкой, которая под них раздвигается: разъехавшись
     по длительности, они читались как два отдельных движения. */
  transition: opacity var(--svc), transform var(--svc);
}
.rig.service .rig-side { opacity: 1; transform: none; }

.rig.service .panel {
  display: flex; flex-direction: column;
  width: auto; margin: 0;
  min-height: 190px;
}
/* min-height: 0 обязателен: без него flex-элемент не сжимается ниже своего
   содержимого, лог растёт вместе с лентой и overflow-y никогда не срабатывает
   — прокрутки нет, а панель разъезжается. */
.rig.service .console-log { flex: 1 1 0; height: auto; min-height: 0; }


/* Узкое окно: колонке не хватает места сбоку, уводим её под машину. */
@media (max-width: 1100px) {
  .rig.service .rig-body { grid-template-columns: minmax(0, 1fr); }
  /* Потолок оставляем: голое width: 100% било по нему и растягивало машину
     на всю колонку, из-за чего приборы под ней уезжали за экран. */
  .stage { width: min(100%, var(--rig-w)); }
  /* Колонка одна, поэтому консоль становится третьей строкой — иначе она
     осталась бы в несуществующей второй колонке. Машина при этом ужимается:
     под ней теперь стоят и лента, и консоль, и на прежнем размере страница
     обзаводилась прокруткой. */
  .rig-side { grid-area: 3 / 1 / 4 / 2; }
  .rig.service { --rig-w: min(46vh * 1.66, 1200px); }
  .rig.service .panel { min-height: 140px; }
}

/* ── Адрес под курсором ───────────────────────────────────────────────────
 * Наводя на бирку или на узел, гость видит, куда попадёт. Браузер показывает
 * это в углу окна мелким шрифтом — здесь адрес идёт за курсором и читается
 * сразу. Держим его тихим: подсказка, а не баннер.
 */
.link-hint {
    position: fixed;
    left: 0; top: 0;
    z-index: 6;
    padding: 7px 11px;
    border-radius: 9px;
    border: 1px solid rgba(147, 161, 161, 0.22);
    background: rgba(6, 22, 27, 0.9);
    backdrop-filter: blur(6px);
    color: rgba(238, 232, 213, 0.92);
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
    font-size: 0.76rem;
    letter-spacing: 0.02em;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transform: translate3d(0, 6px, 0) scale(0.97);
    transition: opacity 0.22s ease, transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.link-hint.on { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
/* Схема протокола глуше самого адреса: она одинаковая у всех ссылок и
   нужна лишь как признак «это адрес». */
.link-hint .lh-scheme { color: rgba(147, 161, 161, 0.55); }
.link-hint .lh-host { color: var(--cyan); }

/* ── Партномера узлов ─────────────────────────────────────────────────────
 * У каждого узла на плате отпечатан хэш коммита, который его последним менял.
 * Клик копирует номер и открывает этот коммит — так же, как по партномеру на
 * живой плате ищут деталь в каталоге.
 */
a.stamp { cursor: pointer; }
a.stamp text { transition: fill-opacity var(--fade); }
a.stamp:hover text { fill-opacity: 0.85; }
a.stamp:focus-visible { outline: none; }
a.stamp:focus-visible text { fill-opacity: 1; }

/* ── Связка узла и подписи ────────────────────────────────────────────────
 * Наводишь на сетевую карту — загорается её подпись, и наоборот. Иначе
 * непонятно, к чему относится ярлык.
 */
/* Подсветка узла и его движение живут на одном элементе, поэтому переход
   перечисляет оба свойства. Шорткат с одним filter затирал transform, и
   всё, что лежит внутри .unit — диски, планки, радиатор, — прыгало в
   конечное положение мгновенно. Вентиляторы при этом ехали плавно: они
   единственные не обёрнуты в .unit, и правило до них не доставало. */
.unit .pick-body, .unit .body { transition: transform var(--glide), filter var(--fade); }
.rig .unit.lit .pick-body,
.rig .unit.lit .body { filter: brightness(1.35) saturate(1.15); }
a.callout.lit .co-box { fill: #fff; stroke: var(--cyan); }
a.callout.lit .co-line { stroke: var(--cyan); }
a.callout.lit .co-dot { fill: var(--head); }

/* ── Лента ревизий ────────────────────────────────────────────────────────
 * Плату собирает код, значит у неё есть история — и по ней можно ходить.
 * Показываем только в сервисном режиме: это инструмент для разобранной
 * машины, на визитке он был бы лишним хромом.
 */
/* Ленты может не быть вовсе: её строит скрипт по истории коммитов, и до
   этого момента на разметке стоит hidden. Класс сам по себе его перебивал. */
.timeline[hidden] { display: none; }

.timeline {
  display: flex;
  grid-column: 1;              /* своей строкой под сценой, не рядом с ней */
  align-items: center;
  gap: 10px;
  margin: 0 auto;
  /* Ровно по сцене: та же доля колонки, тот же потолок. */
  width: min(96%, var(--rig-w));
  /* Вне сервисного режима лента схлопнута в ноль, а не спрятана display:
     так её появление можно вести переходом, и места она при этом не занимает. */
  overflow: hidden;
  opacity: 0;
  max-height: 0;
  padding-top: 0;
  padding-bottom: 0;
  border-width: 0;
  transform: translateY(-10px);
  transition:
    opacity var(--fade), transform var(--glide), max-height var(--glide),
    padding var(--glide), border-width var(--glide), margin var(--glide);
  padding: 8px 12px;
  border: 1px solid var(--edge);
  border-radius: 6px;
  /* Тот же тон, что у консоли: лента — часть той же приборной панели, а не
     отдельная плашка, и полупрозрачный серый на светлом фоне читался грязью. */
  background: linear-gradient(180deg, rgba(9, 34, 40, 0.92) 0%, rgba(7, 26, 31, 0.88) 100%);
  color: var(--fg);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.78rem;
}

.rig.service .timeline {
  opacity: 1; transform: none; margin-top: 14px;
  max-height: 60px; padding: 8px 12px; border-width: 1px;
}

.tl-btn {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border: 1px solid rgba(147, 161, 161, 0.28);
  border-radius: 6px;
  background: transparent;
  color: var(--fg);
  font: inherit;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: border-color var(--fade), color var(--fade);
}

.tl-btn:hover:not(:disabled) { border-color: var(--cyan); color: var(--cyan); }
.tl-btn:disabled { opacity: 0.35; cursor: default; }

/* Ползунок делаем сами: у нативного нет ни одного тона от нашей палитры */
.tl-range {
  flex: 1 1 auto;
  min-width: 0;
  height: 4px;
  appearance: none;
  border-radius: 2px;
  background: linear-gradient(
    to right,
    var(--cyan) calc(var(--tl-pos, 0) * 100%),
    rgba(147, 161, 161, 0.22) calc(var(--tl-pos, 0) * 100%)
  );
  cursor: pointer;
}

.tl-range::-webkit-slider-thumb {
  appearance: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--cyan);
  border: 2px solid var(--bg);
  cursor: pointer;
}

.tl-range::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 2px solid var(--bg);
  border-radius: 50%;
  background: var(--cyan);
  cursor: pointer;
}

.tl-meta {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 8px;
  max-width: 46%;
  color: inherit;
  text-decoration: none;
}

.tl-rev { color: var(--cyan); white-space: nowrap; }

.tl-subject {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.6;
}

.tl-meta:hover .tl-subject { opacity: 0.9; }

/* Пока версия грузится, плата гаснет — иначе непонятно, случилось ли что-то */
.chassis.loading { opacity: 0.55; transition: opacity 0.18s ease; }

@media (max-width: 860px) {
  .tl-meta { display: none; }
}

/* ── screen ──────────────────────────────── */
/* Screen of the machine: POST → BIOS Setup → top.
 *
 * This is the service cover turned into a display. The panel sits in the
 * board's own box, leans into the same plane as the chassis and arrives from
 * above with the motion of a cover being put back on. It used to be a
 * full-page layer, and on a personal card that was wrong: it covered the
 * name, the view switch and the machine itself, while all it has to show is
 * what a monitor plugged into this server would show.
 *
 * Every token here is local on purpose. Inheriting .rig colours would drag
 * the blue of BIOS Setup along with the page theme, and that blue has to stay
 * blue the way it does on a live machine.
 *
 * Sizes inside are in container units (cqw/cqh), not viewport units: the
 * panel is now the size of the machine rather than the size of the window,
 * and vh/vw would set type three times larger than the box holding it.
 */
.crt {
  --crt-ink:   #93a1a1;
  --crt-dim:   #586e75;
  --crt-head:  #eee8d5;
  --crt-green: #859900;
  --crt-cyan:  #2aa198;
  --crt-amber: #b58900;
  --crt-red:   #dc322f;
  --crt-bg:    #041014;

  /* AMI Aptio — a palette of its own, in no way tied to the page theme. */
  --bios-bg:      #00027a;
  --bios-bg2:     #0000aa;
  --bios-border:  #a8a8a8;
  --bios-text:    #d8d8d8;
  --bios-sel-bg:  #d8d800;
  --bios-sel-fg:  #000000;
  --bios-help-bg: #0000c0;

  position: absolute;
  inset: 0;
  z-index: 5;
  /* The panes measure themselves against this box, not against the window. */
  container-type: size;
  box-sizing: border-box;
  padding: 0.7cqw;
  border-radius: 6px;
  background: #161f24;
  border: 1px solid rgba(147, 161, 161, 0.34);
  box-shadow: 0 20px 46px rgba(0, 0, 0, 0.45);
  color: var(--crt-ink);
  font-family: ui-monospace, "SF Mono", Menlo, monospace;

  /* The plane of the machine: the same tilt .chassis settles into. Hidden by
     visibility rather than display, so the slide keeps its transition — and
     so container queries still have a box to measure. */
  /* Origin stays at the default centre so the settled panel lands in exactly
     the plane .chassis sits in; pivoting at the top edge projected it a few
     dozen points lower and wider than the machine underneath. */
  transform: rotateX(13deg) translate3d(0, -108%, 0);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition:
    transform 1.05s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.5s ease,
    visibility 0s linear 1.05s;
}

.crt.on {
  transform: rotateX(13deg);
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition:
    transform 1.05s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.4s ease,
    visibility 0s;
}

/* The glass itself: the cover carries a bezel, the picture lives inside it. */
.crt-glass {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 3px;
  background: var(--crt-bg);
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .crt, .crt.on { transition: opacity 0.2s ease, visibility 0s; }
  .crt { transform: rotateX(13deg); }
}

/* ── POST ─────────────────────────────────────────────────────────────── */
/* Mode panels are switched with the hidden attribute, and each has a display
   of its own — and the browser rule [hidden] { display: none }, with zero
   specificity, loses to it. Without this line the self-test, setup and top are
   visible at once, three windows on top of each other. */
.crt [hidden] { display: none !important; }

.crt-post {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  padding: 5cqh 6cqw;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  font-size: clamp(9px, 1.9cqw, 17px);
  line-height: 1.7;
}
.crt-post-log > div { white-space: pre; }
.crt-post-log .ok    { color: var(--crt-green); }
.crt-post-log .warn  { color: var(--crt-amber); }
.crt-post-log .err   { color: var(--crt-red); }
.crt-post-log .muted { color: var(--crt-dim); }
.crt-post-hint {
  margin-top: 3cqh;
  color: var(--crt-dim);
  font-size: 0.7em;
}

/* ── BIOS Setup ───────────────────────────────────────────────────────── */
.crt-setup {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bios-bg);
  color: var(--bios-text);
  font-size: clamp(8px, 1.5cqw, 15px);
}
.crt-setup-tabs {
  display: flex;
  gap: 2px;
  background: var(--bios-border);
  padding: 4px 4px 0;
}
.crt-tab {
  padding: 4px 18px;
  background: var(--bios-bg2);
  color: var(--bios-text);
  border: 1px solid var(--bios-border);
  border-bottom: none;
}
.crt-tab.active {
  background: var(--bios-bg);
  color: var(--bios-sel-bg);
  font-weight: 600;
}
.crt-setup-body {
  flex: 1;
  min-height: 0;
  display: flex;
  border: 4px solid var(--bios-border);
  border-top: none;
}
.crt-setup-rows {
  flex: 3;
  min-width: 0;
  padding: 1.6cqh 2cqw;
  overflow-y: auto;
}
.crt-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 3px 6px;
}
.crt-row.sel {
  background: var(--bios-sel-bg);
  color: var(--bios-sel-fg);
}
.crt-row.sel .crt-row-value { color: var(--bios-sel-fg); }
.crt-row.editing { outline: 1px dashed var(--bios-sel-bg); outline-offset: -1px; }
.crt-setup-help {
  flex: 2;
  min-width: 0;
  padding: 1.6cqh 1.8cqw;
  background: var(--bios-help-bg);
  border-left: 2px solid var(--bios-border);
  font-size: 0.9em;
}
.crt-setup-note {
  padding: 4px 18px;
  background: var(--bios-border);
  color: #000;
  font-size: 0.8em;
}
.crt-setup-keys {
  padding: 6px 18px;
  background: var(--bios-border);
  color: #000;
  font-size: 0.8em;
  letter-spacing: 0.02em;
}

/* ── top ──────────────────────────────────────────────────────────────── */
.crt-top {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  padding: 4cqh 4cqw;
  display: flex;
  flex-direction: column;
  gap: 3cqh;
}
.crt-top-head {
  color: var(--crt-head);
  font-size: clamp(9px, 1.6cqw, 16px);
  white-space: pre-wrap;
}
.crt-top-grid {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16cqw, 1fr));
  gap: 2cqw;
  align-content: start;
}
.crt-top-card {
  border: 1px solid var(--crt-dim);
  border-radius: 6px;
  padding: 1.6cqh 1.8cqw;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.crt-top-label {
  color: var(--crt-dim);
  font-size: 0.75em;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.crt-top-value {
  color: var(--crt-cyan);
  font-size: clamp(16px, 3.6cqw, 38px);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.crt-top-value.warn { color: var(--crt-amber); }
.crt-top-spark { width: 100%; height: 6cqh; display: block; }
.crt-top-hint {
  color: var(--crt-dim);
  font-size: 0.75em;
  text-align: center;
}

/* ── Firmware's effect on the schematic ───────────────────────────────── */
/* The period has to stay a multiple of a second — otherwise the impeller's
   twenty steps() positions drift off the common beat of 0.05 s. */
.rig.nv-eff { --spin: 2s; }

/* has-fault is already taken by other logic (lamps of missing units), so
   Secure Boot: Disabled gets a flag of its own — sb-off — and repeats the same
   indication trick on fault-sys as has-fault, without touching it. */
.rig.sb-off .fault-sys {
  fill-opacity: 1;
  animation: faultblink var(--beat) steps(1, end) infinite;
}
