/* Start page — product-UI replicas and the scenes built from them.
   ============================================================================
   Loaded only by index.html / de.html, on top of site.css. The product pages do
   not carry it, so they do not pay for it.

   Two halves:
     1. THE UI KIT     — device cards, schematic nodes, logs, trees. Rebuilt
                         from the app's own CSS, using the --ui-* tokens in
                         site.css (which are the app's tokens, converted).
                         Kevin's §9: the page should read as an extension of the
                         software, not as a brochure about it — so these are the
                         real components, not icon-and-caption cards.
     2. THE SCENES     — what the engine in assets/js/home.js drives. Every
                         scene is written in its FINAL state; the `.js-scenes`
                         class (boot.js) is what switches the start state on.
                         Read that file first, the contract is documented there.
*/

/* ════════════════════════════════════════════════════════════════════════════
   1. THE UI KIT
   ════════════════════════════════════════════════════════════════════════════ */

.ui {
  font-family: 'Manrope', system-ui, sans-serif;
  color: var(--ui-text);
  line-height: 1.5;
}
.ui-mono { font-family: var(--font-mono); font-variant-ligatures: none; }

/* ── Surfaces ─────────────────────────────────────────────────────────────── */
.ui-panel {
  background: var(--ui-panel);
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius-lg);
  padding: 14px;
}
.ui-panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ui-faint);
}
.ui-panel-head .ui-mono { text-transform: none; letter-spacing: 0; color: var(--ui-muted); }

/* ── Status ───────────────────────────────────────────────────────────────
   The product never states a status as bare colour: statusColorVar() and
   statusIcon() are always used together, so the four states stay apart for a
   colourblind reader. The replica keeps that — colour AND glyph, every time. */
.ui-status { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 700; }
.ui-status svg { width: 12px; height: 12px; flex: 0 0 auto; fill: currentColor; }
.ui-status.is-online { color: var(--ui-good); }
.ui-status.is-warning { color: var(--ui-warn); }
.ui-status.is-offline { color: var(--ui-bad); }
.ui-status.is-unknown { color: var(--ui-unknown); }

/* The live light from the app shell. Two rhythms, never the same one:
   3.2s on the halo means "this is alive", 1.6s on the dot means "look here". */
.ui-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--ui-unknown);
  transition: background 0.3s ease, box-shadow 0.3s ease;
}
.ui-dot.is-online { background: var(--ui-good); box-shadow: 0 0 0 3px rgb(63 193 104 / 0.25); animation: ui-breathe 3.2s ease-in-out infinite; }
.ui-dot.is-warning { background: var(--ui-warn); box-shadow: 0 0 0 3px rgb(220 188 51 / 0.25); animation: ui-alarm 1.6s ease-in-out infinite; }
.ui-dot.is-offline { background: var(--ui-bad); box-shadow: 0 0 0 3px rgb(241 77 76 / 0.25); animation: ui-alarm 1.6s ease-in-out infinite; }
@keyframes ui-breathe {
  0%, 100% { box-shadow: 0 0 0 3px rgb(63 193 104 / 0.25); }
  50%      { box-shadow: 0 0 0 6px rgb(63 193 104 / 0.08); }
}
@keyframes ui-alarm { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ── Device card ──────────────────────────────────────────────────────────
   frontend/src/components/device/DeviceCard.jsx + screens/Devices.css:317.
   The tell is the 3px status-coloured BOTTOM edge — the app puts status there,
   not on a badge — and the mono footer holding address and last contact. */
.ui-device-card {
  position: relative;
  background: var(--ui-card-alt);
  border: 1px solid var(--ui-border);
  border-bottom: 3px solid var(--ui-unknown);
  border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-shadow);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 5px;
  transition: border-bottom-color 0.45s ease, background 0.2s ease;
}
.ui-device-card.is-online { border-bottom-color: var(--ui-good); }
.ui-device-card.is-warning { border-bottom-color: var(--ui-warn); }
.ui-device-card.is-offline { border-bottom-color: var(--ui-bad); }
.ui-device-card-name {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ui-device-card-sub { font-size: 12px; color: var(--ui-muted); }
/* ⚠️ Nicht --ui-faint wie in der App (Kevin, 19.08.2026: "die sind einfach nur
   schwarz"). Die App zeigt diese Zeile auf einer Karte, die man aus 60 cm
   Abstand liest; auf einer Website mit 11px auf #1c1c1c kommt #5b646f auf
   2,4:1 Kontrast und verschwindet. --ui-muted ist die gleiche Rolle, eine
   Stufe heller. */
.ui-device-card-make { font-family: var(--font-mono); font-size: 11px; color: var(--ui-muted); }
.ui-device-card-foot {
  margin-top: 3px;
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ui-faint);
}

/* Telemetry rows — what a driver actually answers with. */
.ui-telemetry {
  margin: 8px 0 0;
  padding: 9px 0 0;
  border-top: 1px solid var(--ui-border);
  display: grid;
  gap: 3px;
}
.ui-telemetry div {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 11.5px;
  color: var(--ui-muted);
}
/* ⚠️ Tabular figures and a floor under the value column. Telemetry is the one
   place on the page where a number can change between two steps of a scene
   ("Ping 4 ms" → "5 ms"), and a proportional digit would shift the whole card
   sideways while the reader is looking at it. */
.ui-telemetry b {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--ui-secondary);
  font-variant-numeric: tabular-nums;
  min-width: 7ch;
  text-align: right;
}

/* ── Data chip ────────────────────────────────────────────────────────────
   One piece of the installation on its way somewhere: a room name, a model, an
   address, a port. Mono, because in the app every one of these is mono. */
.ui-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--ui-border);
  border-radius: 8px;
  background: var(--ui-card);
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ui-secondary);
  white-space: nowrap;
}
.ui-chip.is-mm { border-color: rgba(var(--mm-rgb), 0.45); color: var(--mm); }
.ui-chip.is-ml { border-color: rgba(var(--ml-rgb), 0.45); color: var(--ml); }

/* ── Spreadsheet ──────────────────────────────────────────────────────────
   Where every one of these projects actually starts. Deliberately plain: thin
   rules, a grey header band, mono cells. */
.ui-sheet {
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius);
  overflow: hidden;
  background: var(--ui-card-alt);
  font-family: var(--font-mono);
  font-size: 11.5px;
}
.ui-sheet-row {
  display: grid;
  /* ⚠️ Re-cut 20.08.2026 for the Boardroom data. The old ratios were sized for
     "201" and "10.20.1.61"; with "Boardroom" and "192.168.100.42" the first and
     fourth columns clipped mid-value, and a truncated IP on a page arguing that
     the address travels with the device is the worst possible ellipsis. */
  grid-template-columns: 0.72fr 0.7fr 1.0fr 0.95fr 1.25fr;
  border-bottom: 1px solid var(--ui-border);
  color: var(--ui-muted);
}
.ui-sheet-row:last-child { border-bottom: 0; }
.ui-sheet-row span { padding: 6px 7px; border-right: 1px solid var(--ui-border); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ui-sheet-row span:last-child { border-right: 0; }
/* Die Kopfzeile steht enger als die Daten: "Installation location" ist laenger
   als jeder Wert darunter, und ein abgeschnittener Spaltenname liest sich als
   Fehler, waehrend ein dichter Kopf ueber weiten Datenzellen genau so aussieht
   wie eine echte Geraeteliste. */
.ui-sheet-row.is-head { background: rgb(255 255 255 / 0.04); color: var(--ui-faint); font-weight: 600; font-size: 0.85em; letter-spacing: -0.01em; }
.ui-sheet-row.is-marked { color: var(--ui-text); background: rgba(var(--mm-rgb), 0.1); box-shadow: inset 2px 0 0 var(--mm); }

/* ── Structure tree ───────────────────────────────────────────────────────── */
.ui-tree { font-family: var(--font-mono); font-size: 12px; display: grid; gap: 2px; }
.ui-tree div { display: flex; align-items: center; gap: 7px; color: var(--ui-muted); padding: 3px 0; }
.ui-tree div span:first-child { color: var(--ui-faint); }
.ui-tree div.is-leaf { color: var(--ui-text); }
.ui-tree .lvl1 { padding-left: 14px; }
.ui-tree .lvl2 { padding-left: 28px; }
.ui-tree .lvl3 { padding-left: 42px; }

/* ── Schematic node + drafting canvas ─────────────────────────────────────
   components/schematic/DeviceBlockNode.css. The signature is the 32px port
   pitch and the connector dot sitting ON the card edge, not inside it. */
.ui-canvas {
  position: relative;
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius);
  background:
    repeating-linear-gradient(0deg, var(--ui-border) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(90deg, var(--ui-border) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(0deg, rgb(255 255 255 / 0.028) 0 1px, transparent 1px 16px),
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.028) 0 1px, transparent 1px 16px),
    var(--ui-deepest);
  overflow: hidden;
}
.ui-node {
  min-width: 168px;
  background: var(--ui-card);
  border: 1px solid var(--ui-border-strong);
  border-radius: var(--ui-radius);
  box-shadow: var(--ui-shadow);
}
.ui-node.is-source { border-color: #d9ba70; }
.ui-node.is-destination { border-color: #ab72d3; }
.ui-node.is-processing { border-color: #3986e4; }
.ui-node.is-infrastructure { border-color: #7d8792; }
.ui-node-head {
  padding: 10px 11px 7px;
  border-bottom: 1px solid var(--ui-border);
  background: rgb(255 255 255 / 0.045);
  border-radius: calc(var(--ui-radius) - 1px) calc(var(--ui-radius) - 1px) 0 0;
}
.ui-node-title { font-size: 12.5px; font-weight: 600; }
.ui-node-model { font-size: 10.5px; color: var(--ui-muted); font-family: var(--font-mono); margin-top: 1px; }
.ui-node-body { display: flex; justify-content: space-between; padding: 10px 0; margin: 0 -1px; }
.ui-node-col { display: grid; }
.ui-port {
  position: relative;
  display: flex;
  align-items: center;
  height: 26px;
  padding: 0 11px;
  font-size: 10.5px;
  color: var(--ui-secondary);
  white-space: nowrap;
}
.ui-port.is-right { justify-content: flex-end; }
/* The dot sits on the border line, exactly as in the designer. */
.ui-port::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--ui-deepest);
  transform: translateY(-50%);
}
.ui-port.is-left::after { left: -5px; background: #1666aa; }
.ui-port.is-right::after { right: -5px; background: var(--ml); }
.ui-port.is-rj45::after { border-radius: 2px; }

/* ── Log / timeline rows ──────────────────────────────────────────────────── */
.ui-log { display: grid; gap: 4px; }
.ui-log-row {
  display: grid;
  grid-template-columns: 62px 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border: 1px solid var(--ui-border);
  border-radius: 8px;
  background: var(--ui-card-alt);
  font-size: 12px;
}
.ui-log-row time { font-family: var(--font-mono); font-size: 10.5px; color: var(--ui-faint); }
.ui-log-row b { font-weight: 600; color: var(--ui-text); }

/* ── Incident card ────────────────────────────────────────────────────────── */
.ui-incident {
  border: 1px solid rgb(241 77 76 / 0.4);
  background: rgb(241 77 76 / 0.07);
  border-radius: var(--ui-radius);
  padding: 12px 14px;
}
.ui-incident-tag { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; color: var(--ui-bad); }
.ui-incident b { display: block; font-size: 14px; margin-top: 4px; }
.ui-incident span { font-size: 12px; color: var(--ui-muted); font-family: var(--font-mono); }

/* ── Kit additions for the rebuilt start page (20.08.2026) ─────────────────
   Small, and deliberately in part 1: they belong to the REPLICA, not to a
   scene. Everything here has to survive Phase 2, when the replicas are swapped
   for real components (website/tools/home/ui-kit.mjs explains the seam). */

/* The status line inside a device card. It used to be wrapped in a per-section
   animation class (.lv-live, .xf-live) — three names for one thing. The card
   is a flex column, so this only needs to claim its own row. */
.ui-card-state { display: flex; }

/* A log row states its severity the same way every other status on the site
   does: by colour AND by position, never by colour alone. The left edge is the
   carrier, so the timestamps stay aligned down the column. */
.ui-log-row.is-warning { box-shadow: inset 2px 0 0 var(--ui-warn); }
.ui-log-row.is-offline { box-shadow: inset 2px 0 0 var(--ui-bad); }
.ui-log-row.is-offline b { color: var(--ui-bad); }

/* ════════════════════════════════════════════════════════════════════════════
   2. THE SCENES
   ════════════════════════════════════════════════════════════════════════════ */

/* Two-colour section framing. `.is-mm` / `.is-ml` set --c so a whole block can
   be tinted by whichever product carries that part of the story. */
.is-mm { --c: var(--mm); --c-rgb: var(--mm-rgb); }
.is-ml { --c: var(--ml); --c-rgb: var(--ml-rgb); }
.tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c, var(--accent));
}
.tag::before { content: ''; width: 18px; height: 2px; background: currentColor; border-radius: 2px; }

/* ── HERO ─────────────────────────────────────────────────────────────────
   Two products, and between them the actual objects that travel. Pure CSS with
   staggered delays — no engine, so it also runs with JavaScript switched off.
   The two lanes are the point: the plan goes right, reality comes back left. */
/* ⚠️ ONE SENTENCE NOW, not three stacked verbs (20.08.2026). The old slogan was
   "Plan it. / Run it. / Know it." — three lines a reader had to map onto two
   products they had not met yet, which is exactly the complaint that started
   this rebuild. The benefit is the headline; the three verbs became the tagline
   under it, where they read as a list instead of as a riddle. */
.hero-slogan { max-width: 18ch; }

/* The capabilities, one line under the benefit. Set in the accent so the eye
   takes it as a label for the sentence above rather than as body copy. */
.hero-tagline {
  margin: 14px 0 0;
  font-size: clamp(17px, 2vw, 22px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--accent);
}
.hero .lead { margin-top: 18px; }

/* Two paragraphs, not one: the claim carries the .q treatment (white, bold) and
   the sentence under it stays quiet. Kevin wrote them apart, and they read
   apart — the first is what the two products ARE, the second is the promise. */
.hero-lead-sub { margin-top: 14px; }
.hero-slogan { line-height: 1.08; }
body .hero { padding-top: 70px; }
.hero .hero-actions { margin-top: 28px; }

.hx {
  display: grid;
  /* The middle column is the run itself, but the two machines need enough room
     that their role line does not break over three lines. */
  grid-template-columns: minmax(0, 1fr) minmax(240px, 1.1fr) minmax(0, 1fr);
  gap: 14px;
  align-items: center;
  margin-top: 40px;
  padding: 26px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background:
    radial-gradient(70% 120% at 0% 50%, rgba(var(--mm-rgb), 0.08), transparent 62%),
    radial-gradient(70% 120% at 100% 50%, rgba(var(--ml-rgb), 0.08), transparent 62%),
    var(--panel);
}
.hx-side { display: flex; flex-direction: column; gap: 10px; }
.hx-side.is-ml { align-items: flex-end; text-align: right; }

/* ⚠️ Both machines FACE THE RUN. The notebook sits at the right edge of the
   left block, the server at the left edge of the right block, so the lane
   between them starts and ends at an icon instead of floating in the middle
   with a gap at either end. Reversing the left block is what does it: the
   markup order stays icon-then-text for both sides. */
.hx-machine { display: flex; align-items: center; gap: 14px; width: 100%; justify-content: space-between; color: var(--c); }
.hx-side.is-mm .hx-machine { flex-direction: row-reverse; }
.hx-machine svg { width: 60px; height: 60px; flex: 0 0 auto; }
.hx-name {
  font-family: 'Poppins', 'Manrope', system-ui, sans-serif;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--fg);
  line-height: 1.15;
}
.hx-name .thin { font-weight: 200; }
.hx-role { font-size: 12px; color: var(--fg-faint); margin-top: 2px; line-height: 1.35; }
.hx-q {
  font-size: 13px;
  font-weight: 700;
  color: var(--c);
  font-family: var(--font-mono);
}

/* "Server" is a qualifier, not part of the wordmark: same size, lighter weight,
   a hair quieter — it explains the name without pretending to be the logo. */
.qual { font-weight: 200; color: var(--fg-dim); margin-left: 0.3em; }

/* ── The run ──────────────────────────────────────────────────────────────
   Two lanes, one per direction. The negative inline margin is what lets them
   reach into the grid gutters and touch the two machines. */
.hx-lanes { display: grid; gap: 18px; margin-inline: -18px; }
.hx-lane-group { display: grid; gap: 4px; }
.hx-lane { position: relative; height: 22px; overflow: hidden; }
.hx-lane::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  /* Near-solid: the run has to READ as a wire from the notebook to the server,
     and a gradient that faded over the first eighth left a visible gap at both
     machines - which is exactly what it was supposed to close. */
  background: linear-gradient(90deg, transparent, var(--line-strong) 3%, var(--line-strong) 97%, transparent);
}
.hx-lane.is-out { --c: var(--mm); --c-rgb: var(--mm-rgb); }
.hx-lane.is-back { --c: var(--ml); --c-rgb: var(--ml-rgb); }
.hx-lane-label {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-faint);
  text-align: center;
}

/* One travelling point. Same 7px dot as the lifecycle band below, with a soft
   coloured halo instead of the white one — restrained on purpose: the brief
   asks for "wenig Glow", and a bright bloom here would be the first thing on
   the page and the least informative. */
.hx-slot { position: absolute; inset: 0; }
.hx-dot {
  position: absolute;
  top: 50%;
  left: 0;
  width: 7px;
  height: 7px;
  margin-top: -3.5px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: 0 0 7px 1px rgba(var(--c-rgb), 0.5);
}

/* Without the engine (or with reduced motion) the points rest on their rail
   rather than piling up at the left edge. */
html:not(.js-scenes) .hx-slot { left: calc(6% + var(--i) * 16%); right: auto; width: 7px; }

.js-scenes .hx-slot {
  animation: hx-run 9s linear infinite;
  animation-delay: calc(var(--i) * -1.5s);   /* negative: the lane is already busy on arrival */
}
.js-scenes .hx-lane.is-back .hx-slot { animation-name: hx-run-back; animation-duration: 11s; }
/* A little variation so the stream is traffic and not a metronome. */
.js-scenes .hx-slot:nth-child(odd) .hx-dot { width: 5px; height: 5px; margin-top: -2.5px; opacity: 0.75; }
.js-scenes .hx-slot:nth-child(3n) { animation-duration: 10.5s; }

@keyframes hx-run {
  0%   { transform: translateX(0); opacity: 0; }
  8%   { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(calc(100% - 7px)); opacity: 0; }
}
@keyframes hx-run-back {
  0%   { transform: translateX(calc(100% - 7px)); opacity: 0; }
  8%   { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(0); opacity: 0; }
}

@media (max-width: 900px) {
  .hx { grid-template-columns: 1fr; gap: 18px; padding: 20px 16px; }
  .hx-side.is-ml { align-items: flex-start; text-align: left; }
  .hx-side.is-mm .hx-machine { flex-direction: row; }
  .hx-machine { justify-content: flex-start; width: auto; }
  .hx-machine svg { width: 48px; height: 48px; }
  .hx-lanes { order: 2; margin-inline: 0; }
  .hx-side.is-ml { order: 3; }
}

/* ── LIFECYCLE RING ───────────────────────────────────────────────────────
   Eight stages on a circle, RESEARCH at twelve o'clock, running clockwise. It
   was a straight chain of pills with a caption underneath insisting it was a
   loop; a row of arrows ends, and the sentence was doing work the shape should
   do. The ring is also this page's navigation — every stage is an anchor. */
.sc-cycle { --c: var(--mm); }
/* 340vh when the ring had eight stages. Six stages, six stops — a third less
   to travel, and the section is no longer the last thing on the page. */
.js-scenes .sc-cycle { min-height: 200vh; }
.cy {
  /* ⚠️ --R and --RL must be LENGTHS, not percentages. A percentage in
     translateY resolves against the element's OWN height — the label is about
     20px tall, so `translateY(-40%)` moved every stage 8px instead of out to
     the rim, and all eight landed in a heap on the hub. Deriving both from
     --size keeps one number in charge of the whole figure.
     --R is the ring itself (the SVG draws r=144 in a 400 box = 36%), --RL is
     where the WORDS sit: outside it, clear of the stroke. */
  /* ⚠️ Die Hoehe gehoert hier genauso hinein wie die Breite. Der Ring sitzt in
     einer 100svh hohen Buehne, und unter ihm stehen jetzt drei reservierte
     Zeilen Erzaehltext — auf einem 800px hohen Laptopfenster ragte die Zeichnung
     dadurch oben unter der Kopfleiste heraus. Gemessen: Buehne braucht rund
     300px zusaetzlich zum Kreis (Innenabstand, Textblock, Abstand). */
  --size: max(300px, min(560px, 86vw, calc(100svh - 300px)));
  --R: calc(var(--size) * 0.36);
  --gap: 28px;                 /* clearance between the stroke and the words */
  --pmax: 48px;                /* how far a label at three o'clock stands out further */
  position: relative;
  width: var(--size);
  aspect-ratio: 1;
  margin: 0 auto;
}
.cy-ring { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.cy-ring circle { fill: none; }
.cy-ring-base { stroke: var(--line); stroke-width: 2; }

/* The travelled arc, in segments — one per run of stages that belong to the
   same product (see sec-lifecycle.mjs). Each circle carries pathLength="1", so
   dasharray and dashoffset are fractions of the way round and --p, which comes
   straight off the engine, can be used without conversion: a segment shows
   `clamp(0, p - from, to - from)` of itself. Rotated so zero is at the top.
   Without the engine every segment is drawn in full, which is the honest end
   state — the whole loop has been travelled. */
.cy-arc {
  stroke-width: 6;
  stroke-linecap: round;
  stroke-dasharray: calc(var(--to) - var(--from)) 1;
  stroke-dashoffset: calc(var(--from) * -1);
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}
.cy-arc.is-mm { stroke: var(--mm); filter: drop-shadow(0 0 6px rgba(var(--mm-rgb), 0.35)); }
.cy-arc.is-ml { stroke: var(--ml); filter: drop-shadow(0 0 6px rgba(var(--ml-rgb), 0.35)); }
/* ⚠️ THE SHARED STAGE. Service belongs to both products (Kevin, 20.08.2026), so
   its stretch of the ring runs FROM orange TO petrol — handing the installation
   back to the plan, which is also the direction the ring travels next. The
   gradient is defined in the SVG (sec-lifecycle.mjs); a third flat colour here
   would read as a third product. */
.cy-arc.is-both { stroke: url(#cy-both); filter: drop-shadow(0 0 6px rgba(var(--mm-rgb), 0.3)); }
.js-scenes .sc-cycle .cy-arc {
  stroke-dasharray: clamp(0, calc(var(--p, 0) - var(--from)), calc(var(--to) - var(--from))) 1;
}

/* ⚠️ A stage is a zero-size anchor AT THE CENTRE, and its two children place
   themselves independently — the marker on the ring, the word outside it. One
   shared transform cannot do that: after `rotate(a) … rotate(-a)` the element's
   axes are page-aligned again, so "further out" is no longer a direction the
   child can express. Both children therefore rotate from the centre themselves,
   each with its own radius. Before this the word sat on the stroke, and with a
   6px ring it was simply unreadable. */
.cy-stage {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  text-decoration: none;
}
.cy-stage.is-mm { --c: var(--mm); --c-rgb: var(--mm-rgb); }
.cy-stage.is-ml { --c: var(--ml); --c-rgb: var(--ml-rgb); }

.cy-pin {
  position: absolute;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--panel);
  border: 3px solid var(--c);
  transform: rotate(var(--a)) translateY(calc(var(--R) * -1)) rotate(calc(var(--a) * -1)) translate(-50%, -50%);
  transition: background 0.35s ease, box-shadow 0.35s ease, opacity 0.35s ease;
}
.cy-label {
  position: absolute;
  width: 140px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--fg);
  white-space: nowrap;
  transform: rotate(var(--a)) translateY(calc((var(--R) + var(--gap) + var(--pf, 0) * var(--pmax)) * -1)) rotate(calc(var(--a) * -1)) translate(-50%, -50%);
  transition: opacity 0.4s ease, color 0.35s ease;
}

/* Reached, and current. Without the engine every stage is simply lit — the
   honest end state, since the whole loop has been travelled. */
.cy-pin { background: var(--c); }
.js-scenes .sc-cycle .cy-pin { background: var(--panel); opacity: 0.45; }
/* ⚠️ The words appear as they are reached (Kevin, 19.08.2026: "sie sollen auch
   erst auftauchen wenn die stufen erreicht sind"). Eight labels standing round
   an empty ring is a legend; eight labels arriving one at a time is the story
   the ring is telling. */
.js-scenes .sc-cycle .cy-label { opacity: 0; }
.js-scenes .sc-cycle .cy-stage.is-past .cy-pin { background: var(--c); opacity: 0.85; }
.js-scenes .sc-cycle .cy-stage.is-past .cy-label { opacity: 1; color: var(--fg-dim); }
.js-scenes .sc-cycle .cy-stage.is-now .cy-pin {
  background: var(--c);
  opacity: 1;
  box-shadow: 0 0 0 7px rgba(var(--c-rgb), 0.15);
}
.js-scenes .sc-cycle .cy-stage.is-now .cy-label { opacity: 1; color: var(--fg); }
.cy-stage:hover .cy-label { color: var(--fg); }

/* The shared stage, on the dot and on the word. Same idea as the arc: a mix of
   the two products rather than a colour of its own. The label falls back to a
   plain colour wherever background-clip:text is not honoured — it must stay
   readable first and clever second. */
.cy-stage.is-both { --c: var(--mm); }
.cy-stage.is-both .cy-pin { background: linear-gradient(135deg, var(--ml), var(--mm)); }
.js-scenes .sc-cycle .cy-stage.is-both.is-past .cy-pin,
.js-scenes .sc-cycle .cy-stage.is-both.is-now .cy-pin { background: linear-gradient(135deg, var(--ml), var(--mm)); opacity: 1; }
/* ⚠️ THE LABEL DOES NOT CARRY THE GRADIENT. It was set with background-clip:
   text and at 12-13px the two ends of the ramp averaged into a washed-out
   yellow-grey that read as "disabled", not as "both". The gradient belongs
   where it has room to be seen — the arc and the dot — and the word stays as
   legible as the other five. Readable first, clever second. */

/* The hub names the stage the ring is on. */
.cy-hub {
  position: absolute;
  inset: 28% 28%;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 10px;
  text-align: center;
}
.cy-hub-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.cy-hub-stage {
  font-size: clamp(24px, 3.2vw, 34px);
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--fg);
  line-height: 1.1;
}
/* All eight names are in the markup; one is shown. The engine already marks
   every [data-at] with is-now, so this needs no per-stage selector list. */
.cy-hub-stage span { display: none; }
.cy-hub-stage span:first-child { display: block; }
.js-scenes .sc-cycle .cy-hub-stage span:first-child { display: none; }
.js-scenes .sc-cycle .cy-hub-stage span.is-now { display: block; }

/* Der Ring traegt acht kurze Saetze statt drei langer, deshalb hier nur die
   reservierte Hoehe: drei Zeilen, gemessen in beiden Sprachen von 390-1600px. */
.sc-cycle .scene-cap { max-width: 34ch; min-height: 4.05em; }

/* ⚠️ On a phone the ring fills the stage, so the words at three and nine
   o'clock are the constraint — they stick out sideways by half their own width
   and pushed the page into a horizontal scroll. Pulling the label radius in and
   narrowing the label buys that back; the ring stays round. */
@media (max-width: 700px) {
  .cy { --gap: 18px; --pmax: 36px; }
  .cy-label { width: 96px; font-size: 12px; }
  .cy-pin { width: 12px; height: 12px; border-width: 2.5px; }
  .cy-arc { stroke-width: 5; }
}
@media (max-width: 480px) {
  .cy { --R: calc(var(--size) * 0.32); --gap: 14px; --pmax: 30px; }
  .cy-label { width: 78px; font-size: 11px; white-space: normal; line-height: 1.2; }
}

/* ── SCENE FRAME ──────────────────────────────────────────────────────────
   A progress scene is a tall block holding a sticky stage: the stage stands
   still on screen while the block scrolls past, and the engine turns that
   distance into --p / data-step. The scroll itself is never taken away. */
/* ⚠️ The PIN must be a direct child of .scene. position:sticky is bounded by the
   parent's box, so wrapping it in the site's usual .wrap would give it a parent
   exactly its own height — and a sticky element with no room to travel simply
   never moves. .scene therefore carries the page gutter itself.
   ⚠️ The pin is a full-viewport box that CENTRES the stage, rather than the
   stage sticking to the top itself. Pinned at the top, a 340px stage left two
   thirds of the screen as empty page for the whole scene (Kevin, 19.08.2026:
   "sie startet erst wenn ich am text vorbei bin und hat dann nur blank page
   darunter"). Centred, the picture is where the reader is looking and there is
   nothing underneath it to explain. */
/* ⚠️ Die Scrollhoehe ist der WEG, ueber den die Schritte laufen — sie ergibt
   nur mit JavaScript einen Sinn. Ohne JS gibt es keine Schritte, nichts wird
   angeheftet, und 220vh reservierte Hoehe waeren schlicht zwei Bildschirme
   leere Seite hinter jeder Tafel. */
.scene {
  position: relative;
  padding: 0 24px;
}
.js-scenes .scene { min-height: 220vh; }
/* ⚠️ Die Kopfleiste klebt oben und liegt UEBER der Buehne. Ohne den Innenabstand
   zentriert die Buehne in vollen 100svh, sitzt also um die halbe Kopfleistenhoehe
   zu hoch und verschwindet mit ihrer Oberkante darunter. Gemessen an allen sechs
   Szenen: gleicher Versatz von 69px ueberall, bei zwei Szenen sichtbar verdeckt.
   Die Kopfleiste ist zweizeilig, sobald sie nicht mehr in eine Zeile passt —
   dieselbe Schwelle wie in site.css. */
:root { --head-h: 69px; }
@media (max-width: 1000px) { :root { --head-h: 112px; } }

/* ⚠️ Angeheftet wird NUR mit JavaScript. Ohne JS gibt es keine Schritte, also
   auch keinen Grund, eine Tafel bildschirmhoch festzuhalten — und vor allem
   keine Einpassung, die sie hineinrechnet. Im Seitenfluss steht dann alles
   ungekuerzt da, was genau der Vertrag dieser Seite ist: das HTML traegt den
   Endzustand allein. */
.scene-pin { display: grid; place-items: center; }
.js-scenes .scene-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  box-sizing: border-box;
  padding-top: var(--head-h);
}
/* ⚠️ The stage is WIDER than the text column, and deliberately so. Body copy
   stops at --maxw because a line longer than ~70 characters is hard to read; a
   picture has no such limit, and at 1750px the 1180px stage sat marooned in the
   middle of the screen with empty page on all four sides (Kevin, 19.08.2026:
   "diese animation hier.. bitte größer.. du hast genug platz"). */
/* ⚠️ DIE BUEHNEN MUESSEN EIN HOEHENBAND TEILEN, sonst zerfaellt die Seite.
   Gemessen am 19.08.2026: 326, 331, 357, 409, 746 und 817px hoch. In einem
   900px-Fenster sitzt die Buehne mittig im Anheftbereich — das ergab mal 280px
   Schwarz ueber und unter der Animation und mal 10px, bei jeder Szene anders.
   Kevin: "am meisten stoert mich dass zwischen animationen und text immer so
   viel schwarz ist... es wirkt nicht mehr wie eine seite dadurch... auch die
   animationen sind alle etwas anderes, da ist kein durchgehender stil dahinter."
   Das Schwarz ist damit nicht weg, aber es liegt INNERHALB der Tafel und ist
   ueberall gleich — ein Rahmen statt einer Leere. align-content zentriert den
   Inhalt darin; ::after ist absolut positioniert und bleibt aussen vor. */
/* Waehrend gemessen wird, darf sich nichts bewegen — sonst misst man den
   Anfang einer Uebergangszeit statt den Zustand. */
.scene.is-measuring, .scene.is-measuring * { transition: none !important; animation: none !important; }

.scene-stage {
  position: relative;
  /* ⚠️ Waagrecht NICHT vom Raster ausrichten lassen. Das Raster zentriert das
     Layoutfeld; gezeichnet wird aber ein um --fit kleineres. Beides zugleich
     ergibt Unsinn — erst schob das Raster die Tafel aus der Mitte, dann kaempfte
     der berechnete Rand dagegen. Die Tafel sitzt links an, und home.js setzt den
     linken Rand aus der GEZEICHNETEN Breite. Senkrecht bleibt das Raster
     zustaendig, dort stimmt das Layoutfeld dank negativem Aussenabstand. */
  justify-self: start;
  transform: scale(var(--fit, 1));
  /* ⚠️ OBEN LINKS, nicht oben mittig. Die eingepasste Tafel wird im Layout um
     1/f breiter gemacht, damit sie gezeichnet wieder genau die Breite ihrer
     Nachbarn hat. Ein Rasterfeld, das breiter ist als sein Kasten, wird aber an
     die ANFANGSKANTE geklemmt statt zentriert — mit Ursprung "mitte" skalierte
     sie dann um ihre eigene, weit rechts liegende Mitte und ragte rechts aus
     dem Fenster: bei "Plan gegen Wirklichkeit" fehlte die halbe Ist-Spalte.
     Mit Ursprung "oben links" bleibt die linke Kante liegen und die gezeichnete
     Breite ist exakt 100%. Senkrecht gilt dasselbe; die Mitte stellt der
     negative Aussenabstand her, nicht der Ursprung. */
  transform-origin: top left;
  width: 100%;
  max-width: 1520px;
  margin: 0 auto;
  display: grid;
  align-content: center;
  padding: 26px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

/* ── The flat panel ───────────────────────────────────────────────────────
   The same frame as a pinned stage, for the seven sections that stand in the
   normal flow. ⚠️ It shares the LOOK only — no transform, no --fit, no
   max-width, because none of those mean anything outside a pin. Before this,
   .ra, .ob, .op and .hd each wrote the same four declarations with slightly
   different padding, which is half of "the sections have no common style". */
.scene-flat {
  padding: 26px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}
@media (min-width: 1100px) {
  .scene-flat { padding: 34px 30px; }
}

/* Once there is real room, the replicas grow with it. The app's own sizes are
   made for a dense working screen; on a landing page seen from further back
   they need a step up, and the scene has the height for it either way. */
@media (min-width: 1100px) {
  .scene-stage { padding: 40px 38px; }
  .xf { grid-template-columns: 1.9fr 36px 0.62fr 36px 1.0fr; gap: 18px; }
  .scene-stage .ui-panel-head { font-size: 13px; margin-bottom: 15px; }
  .scene-stage .ui-tree { font-size: 13.5px; }
  .scene-stage .ui-tree div { padding: 7px 0; }
  .scene-stage .ui-device-card { padding: 20px; gap: 8px; }
  .scene-stage .ui-device-card-name { font-size: 16px; }
  .scene-stage .ui-device-card-sub { font-size: 13px; }
  .scene-stage .ui-device-card-make,
  .scene-stage .ui-device-card-foot { font-size: 12px; }
  .scene-stage .ui-status { font-size: 13px; }
  .scene-stage .ui-status svg { width: 14px; height: 14px; }
  .xf-arrow svg { width: 34px; height: 18px; }
  .scene-cap { font-size: 15px; margin-top: 22px; padding-top: 18px; }

  .pv-head { font-size: 13px; }
  .pv-cell { font-size: 14px; padding: 12px 15px; }
  .pv-op { font-size: 18px; }
  .pv-rows { gap: 10px; }
  /* ⚠️ Plan gegen Wirklichkeit ist die hoechste Buehne der Seite — sieben Zeilen,
   Zeitachse, Kopfzeilen — und sie ragte auf einem 768px hohen Fenster 158px
   heraus. Beschnitten wird dabei UNTEN, und unten steht die letzte Zeile:
   "Room 201 -> Above ceiling · panel 14". Das ist die Pointe der ganzen Seite;
   ausgerechnet die faellt aus dem Bild. Auf niedrigen Fenstern ruecken die
   Zeilen deshalb zusammen, statt dass die Aussage verschwindet. */
@media (max-height: 940px) {
  .sc-pv .scene-stage { padding: 16px 20px; }
  .sc-pv .pv-headline { margin-bottom: 12px; min-height: 2.2em; font-size: clamp(15px, 1.4vw, 17px); }
  .sc-pv .pv { gap: 10px; }
  .sc-pv .pv-rows { gap: 5px; }
  .sc-pv .pv-cell { padding: 6px 10px; font-size: 12px; }
  .sc-pv .pv-verdict { margin-top: 10px; }
}

/* Zweite Stufe fuer 768er-Laptops, wo die erste noch 48px zu wenig sparte. */
@media (max-height: 820px) {
  .sc-pv .scene-stage { padding: 10px 18px; }
  .sc-pv .pv-headline { margin-bottom: 8px; min-height: 1.75em; }
  .sc-pv .pv { gap: 8px; }
  .sc-pv .pv-rows { gap: 3px; }
  .sc-pv .pv-cell { padding: 3px 9px; font-size: 11.5px; }
  .sc-pv .pv-verdict { margin-top: 6px; }
}

/* ⚠️ Unterhalb von 760px Fensterhoehe ist die Zeile nicht mehr durch weniger
   Innenabstand zu retten — es fehlen 57px, und die waeren nur noch aus der
   Lesbarkeit zu holen. Stattdessen wird die ganze Buehne verkleinert: das
   Verhaeltnis bleibt, alle sieben Zeilen bleiben im Bild, und die letzte Zeile
   ist das, worauf die Seite hinauslaeuft. transform aendert das Layoutfeld
   nicht, aber getBoundingClientRect rechnet es mit — die Messung stimmt also. */

.pv-verdict { font-size: 13.5px; padding: 13px 16px; }
  .pv-verdict svg { width: 17px; height: 17px; }
  .pv-proposal li { font-size: 13px; }
  .pv-meta { font-size: 12.5px; }
  .pv-slot { min-height: 148px; }  .os-name { font-size: 17px; }
  .os-sub { font-size: 13px; }  .os-counts b { font-size: 22px; }
  .os-counts span { font-size: 13px; }}
@media (min-width: 1400px) {
  .scene-stage .ui-sheet { font-size: 13px; }
  .scene-stage .ui-sheet-row span { padding: 11px 12px; }
}
@media (max-width: 900px) {
  .scene { padding: 0 18px; }
  .js-scenes .scene { min-height: 200vh; }
  .scene-stage { padding: 18px 16px; }
}
/* At phone width the manufacturer column is the one to go: the model number
   below it already says who made it, and five mono columns in 350px turn every
   address into "10.20.1…". */
/* ⚠️ Unter 1100px faellt der Hersteller weg, unter 560px auch die Adresse.
   Fuenf Mono-Spalten passen dort schlicht nicht mehr, und von den fuenf ist der
   Hersteller die entbehrlichste: das Modell darunter sagt ohnehin, von wem es
   ist. Die Ortsspalte bleibt bis zuletzt stehen - sie ist der Abschnitt. */
@media (max-width: 1100px) {
  .ui-sheet-row { grid-template-columns: 0.42fr 1.1fr 0.85fr 1.25fr; }
  .ui-sheet-row span:nth-child(2) { display: none; }
}
/* Eine zweizeilige Kopfzeile ist in einer Geraeteliste normal; ein
   abgeschnittener Spaltenname liest sich als Fehler. */
@media (max-width: 700px) {
  .ui-sheet-row.is-head span { white-space: normal; line-height: 1.25; }
}
@media (max-width: 560px) {
  .ui-sheet-row { grid-template-columns: 0.4fr 1.15fr 1.3fr; }
  .ui-sheet-row span:nth-child(2),
  .ui-sheet-row span:nth-child(4) { display: none; }
}
/* ══ Die Tafel traegt ihren Text selbst ═════════════════════════════════════
   ⚠️ Ueberschrift und Einleitung standen bis 19.08.2026 AUSSERHALB der Szene,
   im normalen Seitenfluss darueber. Beim Scrollen war der Text dann laengst
   weg, waehrend die angeheftete Tafel allein in einer bildschirmhohen
   schwarzen Flaeche haengen blieb — mal mit 280px Schwarz darum, mal mit 10px,
   bei jeder Szene anders. Kevin: "am meisten stoert mich dass zwischen
   animationen und text immer so viel schwarz ist... es wirkt nicht mehr wie
   eine seite dadurch", und auf die Frage nach der Richtung: "Text mit in die
   Tafel".

   Jede Szene ist damit EINE Einheit: Ansage, Bild, Lehre. Das Schwarz ist
   nicht kleiner geworden, es ist verschwunden — der Platz traegt jetzt den
   Text, der vorher darueber stand.

   ⚠️ Der Preis steht in der Hoehe: Kopf + Bild + Bildtext muessen zusammen in
   einen Bildschirm passen. Deshalb ist die Schrift hier kleiner als im
   Seitenfluss und schrumpft bei niedrigen Fenstern weiter. Wer hier etwas
   vergroessert, drueckt anderswo etwas aus dem Bild — nachmessen. */
.scene-stage .section-head,
.ra .section-head,
.ob .section-head,
.op .section-head,
.draw .section-head {
  max-width: 74ch;
  margin: 0 auto clamp(18px, 2.4vh, 34px);
  text-align: center;
}
.scene-stage .section-head .eyebrow,
.ra .section-head .eyebrow,
.ob .section-head .eyebrow,
.op .section-head .eyebrow,
.draw .section-head .eyebrow { margin-bottom: 10px; }
.scene-stage .section-head h2,
.ra .section-head h2,
.ob .section-head h2,
.op .section-head h2,
.draw .section-head h2 {
  font-size: clamp(22px, 2.5vw, 33px);
  line-height: 1.15;
}
.scene-stage .section-head .lead,
.ra .section-head .lead,
.ob .section-head .lead,
.op .section-head .lead,
.draw .section-head .lead {
  max-width: 74ch;
  margin: 12px auto 0;
  font-size: clamp(15px, 1.25vw, 17.5px);
  line-height: 1.5;
}

/* Bei niedrigen Fenstern zuerst am Text sparen, nicht am Bild: die Animation
   ist das Argument, die Einleitung wiederholt es. */
@media (max-height: 860px) {
  .scene-stage .section-head h2 { font-size: clamp(20px, 2.1vw, 26px); }
  .scene-stage .section-head .lead { font-size: 14.5px; line-height: 1.45; }
  .scene-stage .section-head { margin-bottom: 16px; }
}
@media (max-height: 740px) {
  .scene-stage .section-head .eyebrow { display: none; }
  .scene-stage .section-head h2 { font-size: 19px; }
  .scene-stage .section-head .lead { font-size: 13.5px; }
}

/* ⚠️ .op und .draw sind Raster — ein Kopf darin wuerde sonst eine Spalte
   besetzen statt darueber zu stehen. */
.op .section-head, .draw .section-head { grid-column: 1 / -1; }
/* .draw war als einziger Abschnitt keine Tafel, sondern zwei nackte Bilder im
   Seitenfluss. Sobald der Kopf hineinwandert, muss es eine sein, sonst steht
   dieser eine Abschnitt ohne Rahmen zwischen zehn gerahmten. */
.draw {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 26px 24px;
}
.op { margin-top: 0; }

/* "Beide Produkte" traegt zwei grosse Karten statt einer Tafel — ein Rahmen um
   Rahmen waere hier eine Tafel zu viel. Der Kopf richtet sich trotzdem nach der
   gleichen Achse aus, damit der Abschnitt in der Reihe bleibt.
   ⚠️ Der Kontaktabschnitt (#demo) bleibt bewusst zweispaltig und linksbuendig:
   dort steht kein Argument, sondern ein Formular, und ein zentrierter Kopf ueber
   einer halben Spalte sieht wie ein Versehen aus. */
#products .section-head {
  max-width: 74ch;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}
#products .section-head h2 { font-size: clamp(22px, 2.5vw, 33px); line-height: 1.15; }

/* ⚠️ EIN Bildtext-Stil fuer jede Szene. Es waren drei: .scene-cap klein und
   linksbuendig mit Trennlinie, .lv-lesson/.ob-alert-lesson etwas groesser mit
   eigener Linie, und der Ring seit heute gross und mittig. Drei Behandlungen
   fuer dieselbe Sache sind genau das, was "kein durchgehender Stil" heisst.
   Kevins Fassung vom Ring gewinnt, weil sie die Aussage traegt statt sie zu
   beschriften — und gilt jetzt ueberall. Keine Trennlinie: die Tafel ist der
   Rahmen, eine zweite Kante darin teilt sie nur. */
.scene-cap, .ra-lesson, .dry {
  display: block;
  max-width: 54ch;
  margin: clamp(14px, 2vh, 24px) auto 0;
  padding-top: 0;
  border-top: 0;
  text-align: center;
  font-size: clamp(16px, 1.5vw, 21px);
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: -0.005em;
  color: var(--fg-dim);
  min-height: 3.1em;
}
.scene-cap b, .ra-lesson b, .dry b { color: var(--fg); font-weight: 700; }
/* The dry line. Same one caption style as every other closing line on the page
   — it is the section's last word, not a decoration, so it may not invent a
   sixth text style. It only sits a little quieter and needs no reserved height,
   because a section without a good joke simply has none (Kevin, 20.08.2026:
   the humour is a tone rule, not a count). */
.dry { min-height: 0; color: var(--fg-faint); font-weight: 500; font-size: clamp(15px, 1.3vw, 18px); }
.scene-cap b { color: var(--fg); font-weight: 700; }
/* One caption line per stage: the current one is shown, the rest wait. Without
   the engine every line is visible, which reads as a list of what happens. */
.js-scenes .scene-cap > span { display: none; }
.js-scenes .scene[data-step="0"] .scene-cap > span:nth-child(1),
.js-scenes .scene[data-step="1"] .scene-cap > span:nth-child(2),
.js-scenes .scene[data-step="2"] .scene-cap > span:nth-child(3),
.js-scenes .scene[data-step="3"] .scene-cap > span:nth-child(4),
.js-scenes .scene[data-step="4"] .scene-cap > span:nth-child(5),
.js-scenes .scene[data-step="5"] .scene-cap > span:nth-child(6),
.js-scenes .scene[data-step="6"] .scene-cap > span:nth-child(7),
.js-scenes .scene[data-step="7"] .scene-cap > span:nth-child(8) { display: inline; }

/* Progress hairline along the top of the stage. */
.scene-stage::after {
  content: '';
  position: absolute;
  left: 0;
  top: -1px;
  height: 2px;
  width: calc(var(--p, 1) * 100%);
  background: var(--c, var(--accent));
  border-radius: 2px;
  opacity: 0.8;
}

/* ── SCENE: spreadsheet → structure → device ──────────────────────────────
   Three panels and two arrows. What travels is not a picture of a row, it is
   the row itself: the same accent marks it in the sheet, in the tree and on the
   card, so the eye follows one object through three representations. */
.sc-design { --c: var(--mm); }
/* Four steps, the fewest of the four pinned scenes — so the shortest travel.
   ⚠️ Measured, not guessed (20.08.2026): at 220vh the page came to 19.5 screens
   at 1440x900, and roughly 45vh per step is what the other scenes settled on. */
.js-scenes .sc-design { min-height: 180vh; }
.xf {
  display: grid;
  grid-template-columns: 1.9fr 30px 0.62fr 30px 1.0fr;
  align-items: center;
  gap: 14px;
}
.xf-step { transition: opacity 0.45s ease, transform 0.45s ease, filter 0.45s ease; }
.xf-arrow { color: var(--ui-faint); display: flex; justify-content: center; transition: color 0.45s ease; }
.xf-arrow svg { width: 26px; height: 14px; }

/* Start state: only the sheet is lit. Everything else waits, a little back and
   a little quieter — never hidden, so the layout never jumps.
   ⚠️ "Quieter" was 0.22 opacity plus a saturation drop, and that was too far
   (Kevin, 19.08.2026: "die cards auf der rechten seite im bild sind kaum
   leserlich"). A panel nobody can read does not say "this step has not happened
   yet", it says the page is broken — and the reader has to scroll before they
   find out otherwise. The waiting state is legible now, and which step is
   CURRENT is said by the panel label lighting up instead. */
.js-scenes .sc-design .xf-step:not(:first-child) { opacity: 0.45; transform: scale(0.985); }
.xf-step .ui-panel-head { transition: color 0.4s ease; }
.js-scenes .sc-design[data-step="1"] .xf-step:nth-child(3),
.js-scenes .sc-design[data-step="2"] .xf-step:nth-child(3),
.js-scenes .sc-design[data-step="3"] .xf-step:nth-child(3) { opacity: 1; transform: none; filter: none; }
.js-scenes .sc-design[data-step="2"] .xf-step:nth-child(5),
.js-scenes .sc-design[data-step="3"] .xf-step:nth-child(5) { opacity: 1; transform: none; filter: none; }
.js-scenes .sc-design[data-step="1"] .xf-arrow:nth-child(2),
.js-scenes .sc-design[data-step="2"] .xf-arrow:nth-child(2),
.js-scenes .sc-design[data-step="3"] .xf-arrow:nth-child(2) { color: var(--mm); }
.js-scenes .sc-design[data-step="2"] .xf-arrow:nth-child(4),
.js-scenes .sc-design[data-step="3"] .xf-arrow:nth-child(4) { color: var(--ml); }

/* The tree grows level by level rather than appearing at once — but only
   SIDEWAYS. It must not start at opacity 0: the panel around it is already
   dimmed at that stage, so an invisible tree left the middle column showing
   nothing but its label, which reads as a broken layout rather than a step that
   has not happened yet. */
.js-scenes .sc-design .ui-tree div { transform: translateX(-10px); transition: transform 0.4s ease; }
.js-scenes .sc-design[data-step="1"] .ui-tree div,
.js-scenes .sc-design[data-step="2"] .ui-tree div,
.js-scenes .sc-design[data-step="3"] .ui-tree div { transform: none; transition-delay: calc(var(--i) * 0.12s); }

/* The card is documentation until it has been polled; then it starts answering.
   ⚠️ The status appears at step 2, when the card does — not at step 3, which is
   the schematic. Before that the card is the app's own "unknown" grey, which is
   what a device looks like the moment it has been typed in and never contacted. */
.js-scenes .sc-design .ui-card-state { opacity: 0; transition: opacity 0.5s ease; }
.js-scenes .sc-design[data-step="2"] .ui-card-state,
.js-scenes .sc-design[data-step="3"] .ui-card-state { opacity: 1; }
.js-scenes .sc-design:not([data-step="2"]):not([data-step="3"]) .ui-device-card { border-bottom-color: var(--ui-unknown); }

@media (max-width: 900px) {
  .xf { grid-template-columns: 1fr; gap: 10px; }
  .xf-arrow { transform: rotate(90deg); }
}

/* ── Counts, shared ───────────────────────────────────────────────────────
   A row of "12 devices / 37 connections / 4 rooms". Used by the handover panel
   and by the sync proposal in the comparison — it survived the removal of the
   on-site scene it was written for, so it lives here on its own now.
   .os-name / .os-sub go with it: they label the two machines either side. */
.os-name { font-size: 15px; font-weight: 700; color: var(--fg); line-height: 1.25; }
.os-sub { font-size: 12.5px; color: var(--fg-faint); margin-top: 2px; font-family: var(--font-mono); }
.os-counts { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 9px; }
.os-counts div { display: flex; align-items: baseline; gap: 7px; }
.os-counts b { font-family: var(--font-mono); font-size: 19px; font-weight: 600; color: var(--ui-text); }
.os-counts span { font-size: 12.5px; color: var(--ui-muted); }

/* ── SCENE: plan against reality, over years ──────────────────────────────
   A clock along the top, and differences that accumulate under it. The static
   version had no time in it, so it could not say WHY documentation drifts —
   and the why is the argument. */
.sc-pv { --c: var(--ml); }
/* 300vh before the timeline moved out to section 08 (20.08.2026). Five stages
   still, but one row less to read at each of them. */
.js-scenes .sc-pv { min-height: 200vh; }

/* The clock. The rail fills as the years pass; each mark is a date. */
/* The default above is the whole clock travelled, which is what a reader
   without the engine sees. With it, the marks light as they are reached. */

/* What happened at this point in time. */
.pv-headline {
  margin: 0 0 20px;
  min-height: 2.6em;
  font-size: clamp(16px, 1.6vw, 20px);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--fg);
  line-height: 1.3;
}
/* One headline per stage; the engine marks the current [data-at] with is-now,
   so this needs no per-stage selector list. */
.pv-headline span { display: none; }
.pv-headline span:nth-child(4) { display: block; }   /* stage 3 = the state in the HTML */
.js-scenes .sc-pv .pv-headline span:nth-child(4) { display: none; }
.js-scenes .sc-pv .pv-headline span.is-now { display: block; }

.pv { display: grid; gap: 16px; }
.pv-heads { display: grid; grid-template-columns: 1fr 148px 1fr; gap: 12px; }
.pv-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c);
  padding-left: 11px;
  border-left: 3px solid var(--c);
}
.pv-head.is-right { text-align: right; padding-left: 0; padding-right: 11px; border-left: 0; border-right: 3px solid var(--c); }
.pv-rows { display: grid; gap: 8px; }
.pv-row {
  display: grid;
  grid-template-columns: 1fr 148px 1fr;
  align-items: center;
  gap: 12px;
}
.pv-cell {
  font-family: var(--font-mono);
  font-size: 12.5px;
  padding: 9px 12px;
  border: 1px solid var(--ui-border);
  border-radius: 9px;
  background: var(--ui-card-alt);
  color: var(--ui-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: border-color 0.4s ease, color 0.4s ease, background 0.4s ease;
}
.pv-cell.is-right { text-align: right; }

/* The middle column names the field. Without it the reader has to work out what
   "Rack 02 / HE14" is a value OF. */
.pv-mid { display: grid; justify-items: center; gap: 2px; }
.pv-field {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ui-faint);
}
.pv-op {
  font-family: var(--font-mono);
  font-size: 15px;
  font-weight: 600;
  color: var(--ui-faint);
  transition: color 0.4s ease;
}
.pv-op i { font-style: normal; }
.pv-op .op-diff { display: none; }

/* A row that has parted: both cells amber, the operator flips.
   ⚠️ Written as the DEFAULT, because the state in the HTML is stage 3 — see the
   header of sec-perspectives.mjs for why this scene, unlike the others, does
   not carry its LAST frame. */
.pv-row[data-diff-at] .pv-cell {
  border-color: rgba(220, 188, 51, 0.55);
  color: var(--ui-warn);
  background: rgb(220 188 51 / 0.07);
}
.pv-row[data-diff-at] .pv-op { color: var(--ui-warn); }
.pv-row[data-diff-at] .pv-op .op-same { display: none; }
.pv-row[data-diff-at] .pv-op .op-diff { display: inline; }

/* ── Which rows are still in agreement, stage by stage ────────────────────
   At stage 0 all of them; at 1 the ones that part later; at 4 all of them
   again, because the sync has just settled every one. Five selectors describe
   the whole clock, instead of one rule per row per stage. */
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="4"] .pv-row[data-diff-at]) .pv-cell {
  border-color: var(--ui-border);
  color: var(--ui-secondary);
  background: var(--ui-card-alt);
}
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="4"] .pv-row[data-diff-at]) .pv-op { color: var(--ui-faint); }
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="4"] .pv-row[data-diff-at]) .op-same { display: inline; }
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="4"] .pv-row[data-diff-at]) .op-diff { display: none; }

/* Which reading each cell shows. Before a row parts, the installation still
   holds the planned value; after the sync, the PLAN holds the real one. */
.pv-was, .pv-plan-new { display: none; }
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"]) .pv-was { display: inline; }
:is(.js-scenes .sc-pv[data-step="0"] .pv-row[data-diff-at],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="2"],
    .js-scenes .sc-pv[data-step="1"] .pv-row[data-diff-at="3"],
    .js-scenes .sc-pv[data-step="2"] .pv-row[data-diff-at="3"]) .pv-now { display: none; }
.js-scenes .sc-pv[data-step="4"] .pv-plan-old { display: none; }
.js-scenes .sc-pv[data-step="4"] .pv-plan-new { display: inline; }

/* The verdict. Three of them, and the line is never empty. */
.pv-verdict {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid rgba(220, 188, 51, 0.45);
  background: rgb(220 188 51 / 0.08);
  border-radius: 9px;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--ui-warn);
  transition: color 0.4s ease, border-color 0.4s ease, background 0.4s ease;
}
.pv-verdict svg { width: 15px; height: 15px; fill: currentColor; flex: 0 0 auto; }
.pv-verdict span { display: inline-flex; align-items: center; gap: 10px; }
.pv-verdict .pv-verdict-ok,
.pv-verdict .pv-verdict-synced { display: none; }
:is(.js-scenes .sc-pv[data-step="0"], .js-scenes .sc-pv[data-step="4"]) .pv-verdict {
  border-color: rgba(63, 193, 104, 0.4);
  background: rgb(63 193 104 / 0.07);
  color: var(--ui-good);
}
:is(.js-scenes .sc-pv[data-step="0"], .js-scenes .sc-pv[data-step="4"]) .pv-verdict .pv-verdict-diff { display: none; }
.js-scenes .sc-pv[data-step="0"] .pv-verdict .pv-verdict-ok { display: inline-flex; }
.js-scenes .sc-pv[data-step="4"] .pv-verdict .pv-verdict-synced { display: inline-flex; }

/* ── Which way the data went ──────────────────────────────────────────────
   Only at the sync, and only then: the rest of the scene is a comparison, not
   a transfer. The arrow points at the PLAN column, because the installation is
   what is true and the drawing is what gets corrected. */
.pv-flow {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--mm);
  opacity: 0;
  transform: translateX(14px);
  transition: opacity 0.5s ease, transform 0.5s ease;
  height: 0;
  overflow: hidden;
}
/* Head and line are separate: a single stretched SVG squashed the arrowhead
   flat, because preserveAspectRatio="none" is what lets the line span. */
.pv-flow-head { width: 12px; height: 12px; flex: 0 0 auto; }
.pv-flow-line {
  flex: 1 1 auto;
  min-width: 0;
  height: 1px;
  margin-left: -4px;
  background: repeating-linear-gradient(90deg, currentColor 0 7px, transparent 7px 13px);
}
.pv-flow b { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; }
.js-scenes .sc-pv[data-step="4"] .pv-flow { opacity: 1; transform: none; height: 22px; }

/* One slot, two occupants — see the markup comment in sec-perspectives.mjs. */
.pv-slot { position: relative; min-height: 132px; }
.pv-meta { margin: 0; font-family: var(--font-mono); font-size: 11.5px; color: var(--ui-faint); }
.js-scenes .sc-pv[data-step="4"] .pv-slot .pv-meta { display: none; }

/* What the sync wrote back — the half of the story every rendered picture of
   this has left out so far: the correction travels to the PLAN. */
.pv-proposal {
  position: absolute;
  inset: 0 auto auto 0;
  right: 0;
  border: 1px solid rgba(var(--mm-rgb), 0.45);
  background: rgba(var(--mm-rgb), 0.07);
  border-radius: var(--ui-radius);
  padding: 13px 15px;
  max-width: 640px;
  display: none;
}
.pv-proposal-tag { font-size: 11px; font-weight: 700; letter-spacing: 0.1em; color: var(--mm); }
.pv-proposal-note { margin: 10px 0 0; font-family: var(--font-mono); font-size: 12px; color: var(--ui-secondary); }
.pv-proposal-act { margin-top: 11px; }
.js-scenes .sc-pv[data-step="4"] .pv-proposal { display: block; }

@media (max-width: 760px) {
  .pv-heads, .pv-row { grid-template-columns: 1fr 84px 1fr; gap: 7px; }
  .pv-cell { font-size: 10px; padding: 7px 8px; }
  .pv-field { font-size: 9px; letter-spacing: 0.04em; }
  .pv-op { font-size: 13px; }
  .pv-slot { min-height: 130px; }
}

/* ── THE DRAWINGS ─────────────────────────────────────────────────────────
   Stage 2b, and the answer to the section above it: a spreadsheet cell can hold
   "Room 201", a drawing can hold a place. Real screenshots rather than replicas
   — for the designers the original carries more than anything rebuilt in CSS,
   and both images already exist for the monomanager page. */
.draw { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }/* Beide Aufnahmen kommen aus verschiedenen Ansichten und haben verschiedene
   Seitenverhaeltnisse; ohne festen Rahmen starten die Beschriftungen versetzt
   und die beiden Spalten lesen sich nicht mehr als Paar. */* ⚠️ Not decoration: RoomSketch is published:false, and this note is the
   condition under which it may appear on the site at all. See the header of
   sec-design.mjs. */

@media (max-width: 900px) {
  .draw { grid-template-columns: 1fr; gap: 34px; }
}

/* ── SCENE: the handover ──────────────────────────────────────────────────
   Stage 3. The finished project crosses to the customer's server in one pass.
   Only the package moves, and only by transform — the rail, both machines and
   the stage height stay put, so nothing reflows while the reader scrolls. */
.sc-deploy { --c: var(--mm); }
/* ⚠️ NO min-height ANY MORE (20.08.2026): this scene is time-driven now, so it
   claims no scroll height at all. The .dp rules below are unchanged and keep
   working, because the section kept its class name — only the pin is gone. */
.dp {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(260px, 1.5fr) minmax(0, 0.85fr);
  gap: 16px;
  align-items: center;
}
.dp-side { display: flex; align-items: center; gap: 14px; }
.dp-side.is-mm { --c: var(--mm); color: var(--c); }
.dp-side.is-ml { --c: var(--ml); color: var(--c); flex-direction: row-reverse; text-align: right; }
.dp-side svg { width: 56px; height: 56px; flex: 0 0 auto; }

.dp-lane { position: relative; padding: 26px 0; }
.dp-rail {
  position: absolute;
  left: -16px;
  right: -16px;
  top: 50%;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-strong) 4%, var(--line-strong) 96%, transparent);
}
.dp-pkg {
  position: relative;
  border: 1px solid rgba(var(--mm-rgb), 0.45);
  background: rgba(var(--mm-rgb), 0.07);
  border-radius: var(--ui-radius);
  padding: 14px 16px;
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.5s ease, background 0.5s ease;
}
.dp-pkg-tag {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mm);
  margin-bottom: 10px;
  transition: color 0.5s ease;
}
/* Arrived: the package is the customer's now, so it takes MonoLog's colour.
   Without the engine it simply sits there, already across — the end state. */
.js-scenes .sc-deploy[data-step="0"] .dp-pkg { transform: translateX(-6%); }
.js-scenes .sc-deploy[data-step="1"] .dp-pkg { transform: translateX(0); }
.sc-deploy[data-step="2"] .dp-pkg,
html:not(.js-scenes) .sc-deploy .dp-pkg {
  transform: translateX(6%);
  border-color: rgba(var(--ml-rgb), 0.45);
  background: rgba(var(--ml-rgb), 0.07);
}
.sc-deploy[data-step="2"] .dp-pkg-tag { color: var(--ml); }

@media (min-width: 1100px) {
  .dp-side svg { width: 64px; height: 64px; }
  .dp-pkg { padding: 18px 20px; }
}
@media (max-width: 900px) {
  .dp { grid-template-columns: 1fr; gap: 18px; }
  .dp-side.is-ml { flex-direction: row; text-align: left; }
  .dp-lane { order: 2; }
  .dp-side.is-ml { order: 3; }
  .dp-rail { display: none; }
  .js-scenes .sc-deploy .dp-pkg,
  .sc-deploy[data-step="2"] .dp-pkg { transform: none; }
}

/* ── Room automation ──────────────────────────────────────────────────────
   Time-driven, not scroll-driven: a sequence has its own clock, and tying it to
   the scrollbar would let the reader run a room ON at whatever speed they
   happen to scroll. It therefore needs no scroll height either, which keeps the
   page from becoming five sticky scenes in a row. */
.ra {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 26px 24px;
}
.ra-head { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
.ra-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 11px 20px;
  border-radius: 10px;
  border: 1px solid rgba(var(--ml-rgb), 0.5);
  background: rgba(var(--ml-rgb), 0.1);
  color: var(--ml);
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s ease, border-color 0.2s ease;
}
.ra-btn:hover { background: rgba(var(--ml-rgb), 0.16); border-color: var(--ml); }
.ra-btn-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
/* Three labels, one shown. Idle before the run, "running" during it, "room
   ready" after — the button is the status, so nothing else has to say it. */
.ra-btn-run, .ra-btn-done { display: none; }
.js-scenes .ra:not([data-step]) .ra-btn-idle { display: inline; }
.js-scenes .ra[data-step] .ra-btn-idle { display: none; }
.js-scenes .ra[data-step] .ra-btn-run { display: inline; }
.js-scenes .ra[data-step="6"] .ra-btn-run { display: none; }
.js-scenes .ra[data-step="6"] .ra-btn-done { display: inline; }
.js-scenes .ra[data-step="6"] .ra-btn-dot { animation: none; }
.js-scenes .ra[data-step]:not([data-step="6"]) .ra-btn-dot { animation: ui-alarm 1.2s ease-in-out infinite; }
.ra-replay { font-size: 13px; color: var(--fg-faint); }

.ra-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.ra-step {
  display: grid;
  grid-template-columns: 26px 120px 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 11px 14px;
  border: 1px solid var(--ui-border);
  border-radius: 9px;
  background: var(--ui-card-alt);
  transition: opacity 0.3s ease, border-color 0.3s ease, background 0.3s ease;
}
.ra-step-n {
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--chip, rgb(255 255 255 / 0.05));
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ui-faint);
}
.ra-step-dev { font-size: 13px; font-weight: 700; color: var(--ui-text); }
.ra-step-what { font-size: 13px; color: var(--ui-muted); }
.ra-step-ok { display: inline-flex; align-items: center; gap: 7px; color: var(--ui-good); font-size: 12px; font-weight: 700; }
.ra-step-ok svg { width: 13px; height: 13px; fill: currentColor; }
.ra-step-ok em { font-style: normal; font-family: var(--font-mono); color: var(--ui-faint); font-weight: 400; }

/* Steps that have not run yet are dimmed and have no result. Without the engine
   the whole sequence stands complete, which is the honest end state. */
.js-scenes .ra .ra-step { opacity: 0.35; }
.js-scenes .ra .ra-step .ra-step-ok { visibility: hidden; }
.js-scenes .ra .ra-step.is-past,
.js-scenes .ra .ra-step.is-now { opacity: 1; }
.js-scenes .ra .ra-step.is-past .ra-step-ok,
.js-scenes .ra .ra-step.is-now .ra-step-ok { visibility: visible; }
.js-scenes .ra .ra-step.is-now { border-color: rgba(var(--ml-rgb), 0.5); background: rgba(var(--ml-rgb), 0.06); }
.js-scenes .ra .ra-step.is-now .ra-step-n { color: var(--ml); }


@media (max-width: 760px) {
  .ra { padding: 18px 16px; }
  .ra-step { grid-template-columns: 22px 1fr auto; gap: 10px; padding: 9px 11px; }
  .ra-step-what { display: none; }
  .ra-step-n { width: 22px; height: 22px; }
}

/* ── In operation ─────────────────────────────────────────────────────────
   Stage 5. Four cards of the room the whole page is about, coming up one after
   another, and one of them reporting real telemetry. Time-driven: a poll has
   its own rhythm and does not belong on the scrollbar. */
/* One .ui-* leftover that belongs to the kit, not to a scene: the label column
   inside a telemetry block sits one step quieter than its value. */
.ui-telemetry span { color: var(--ui-faint); }

/* ── Where it runs ────────────────────────────────────────────────────────
   One building, and what is outside it. The crossed-out cloud has to stay
   literal — see the header of sec-onprem.mjs for why this drawing keeps
   drifting towards making the cloud a partner. */
.op {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(240px, 1.2fr) minmax(0, 0.9fr);
  gap: 20px;
  align-items: center;
  margin-top: 34px;
  padding: 26px 24px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}
.op-outside { display: grid; gap: 14px; }
.op-outside.is-right { justify-items: end; text-align: right; }
.op-node {
  display: grid;
  gap: 3px;
  padding: 12px 14px;
  border: 1px dashed var(--line-strong);
  border-radius: var(--ui-radius);
  max-width: 250px;
}
.op-outside.is-right .op-node { justify-items: end; }
/* Die einzige Verbindung im Bild, und sie fuehrt INS Gebaeude. Ohne sie steht
   der Integrator nur daneben und die Zeile "erreicht ihn ueber das Netz des
   Kunden" bleibt eine Behauptung neben einer Zeichnung, die sie nicht zeigt. */
.op-outside.is-left .op-node { position: relative; border-color: rgba(var(--mm-rgb), 0.4); }
.op-outside.is-left .op-node .op-node-tag { color: var(--mm); }
.op-outside.is-left .op-node::after {
  content: '';
  position: absolute;
  left: 100%;
  top: 50%;
  width: 22px;
  height: 1px;
  background: repeating-linear-gradient(90deg, var(--mm) 0 5px, transparent 5px 10px);
  opacity: 0.7;
}
.op-node svg { width: 34px; height: 26px; color: var(--fg-faint); margin-bottom: 4px; }
.op-node-tag { font-size: 12.5px; font-weight: 700; color: var(--fg-dim); }
.op-node-note { font-size: 12px; color: var(--fg-faint); line-height: 1.4; }
.op-node.is-cloud { border-color: rgba(242, 99, 79, 0.35); }
.op-node.is-cloud .op-node-tag { color: var(--bad); }
.op-node.is-cloud svg { color: var(--bad); }
/* ⚠️ The stroke through the cloud is the claim. It is drawn, not implied by a
   colour: on a dark page a "reddish" cloud reads as a warning, not as "this
   does not exist in the path". */
.op-cross { stroke-width: 2.2; }
.op-node.is-mail svg { color: var(--ok); }

/* The building. Everything that matters is inside it, which is the point, so it
   gets the only solid border in the picture. */
.op-building {
  position: relative;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--ui-radius-lg);
  padding: 26px 20px 20px;
  background:
    repeating-linear-gradient(0deg, rgb(255 255 255 / 0.02) 0 1px, transparent 1px 18px),
    var(--ui-panel);
}
.op-building-tag {
  position: absolute;
  top: -9px;
  left: 16px;
  padding: 0 8px;
  background: var(--panel);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-faint);
}
.op-server {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid rgba(var(--ml-rgb), 0.4);
  background: rgba(var(--ml-rgb), 0.07);
  border-radius: var(--ui-radius);
}
.op-server b { display: block; font-size: 14px; color: var(--ui-text); }
.op-server span { font-family: var(--font-mono); font-size: 12px; color: var(--ui-muted); }

.op-points {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
}
.op-points li { display: grid; gap: 6px; }
.op-points b { font-size: 15px; color: var(--fg); }
.op-points span { font-size: 14px; color: var(--fg-dim); line-height: 1.6; }

@media (max-width: 900px) {
  .op { grid-template-columns: 1fr; gap: 16px; padding: 20px 16px; }
  .op-outside.is-left .op-node::after { display: none; }
  .op-outside.is-right { justify-items: start; text-align: left; }
  .op-outside.is-right .op-node { justify-items: start; }
  .op-node { max-width: none; }
  .op-points { grid-template-columns: 1fr; gap: 18px; }
}

/* ── Product doors ────────────────────────────────────────────────────────── */
/* ── 03 Design: the schematic strip ───────────────────────────────────────
   ⚠️ In the layout from step 0, revealed by opacity only. A pinned stage that
   changes height is a stage fit() scales for its tallest state and then leaves
   under-filled for every other one. */
.sd { margin-top: 18px; }
.sd-canvas { padding: 18px 16px; }
.sd-row { display: flex; align-items: center; justify-content: center; gap: 0; flex-wrap: wrap; }
/* The signal between two blocks. A CSS rule in the gap, not a computed path —
   see the note in ui-kit.mjs. */
.sd-link {
  flex: 1 1 46px;
  max-width: 96px;
  min-width: 22px;
  height: 2px;
  background: linear-gradient(90deg, rgba(var(--ml-rgb), 0.25), rgba(var(--ml-rgb), 0.85));
}
.js-scenes .sc-design .sd { opacity: 0; transition: opacity 0.5s ease; }
.js-scenes .sc-design .sd.is-now,
.js-scenes .sc-design .sd.is-past { opacity: 1; }

.sd-close { margin-top: 30px; text-align: center; }
.sd-close .claim {
  border-left: 0;
  padding-left: 0;
  margin: 0 auto;
  max-width: 22ch;
  font-size: clamp(22px, 2.8vw, 32px);
}

@media (max-width: 760px) {
  .sd-row { flex-direction: column; }
  /* Stacked, the link becomes a short vertical drop. */
  .sd-link { flex: 0 0 22px; width: 2px; height: 22px; background: linear-gradient(180deg, rgba(var(--ml-rgb), 0.25), rgba(var(--ml-rgb), 0.85)); }
}

/* ── 10 close: the ring hands back to the plan ────────────────────────────── */
.cy-close { margin-top: 30px; text-align: center; }
.cy-close-line {
  max-width: 56ch;
  margin: 0 auto;
  font-size: clamp(16px, 1.5vw, 19px);
  font-weight: 600;
  color: var(--fg-dim);
}

/* ── 09 The Friday call ───────────────────────────────────────────────────
   Two halves, one device. ⚠️ Build it two-column from the start and keep the
   lead to two lines: this is the tallest pinned stage on the page, and fit()
   bottoms out at MIN_FIT 0.62 — past that the stage is not fitted any more, it
   is shrunk. Check the computed --fit at 1366x768 before adding a line here. */
.sc-friday { --c: var(--ml); }
.js-scenes .sc-friday { min-height: 200vh; }

/* The eight questions a service call usually opens with. They are the reason
   the two panels mean anything, so they come FIRST and stay visible. */
.fr-questions { margin-bottom: 18px; }
.fr-questions-tag {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-faint);
  margin-bottom: 9px;
}
.fr-questions ul { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; padding: 0; list-style: none; }
.fr-q {
  padding: 6px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-2);
  font-size: 12.5px;
  color: var(--fg-dim);
  transition: color 0.3s ease, border-color 0.3s ease, opacity 0.3s ease;
}
/* Answered: struck through and stood down. ⚠️ The strike is on top of a colour
   change, not instead of it — a line through grey text at 12.5px is easy to
   miss, and the whole beat of the section is that these stop being questions.
   ⚠️ DRIVEN PURELY BY [data-step], NOT by a class the engine sets. home.js only
   ever marks elements carrying `data-at`, and a question is not a stage — it is
   answered BY one. So data-answered-at is read by these selectors alone, which
   means the whole beat works with no change to the engine. CSS has no ">=", so
   each later step repeats the earlier groups; that is the cost of keeping the
   engine untouched, and it is four extra selectors. */
.js-scenes .sc-friday[data-step="1"] .fr-q[data-answered-at="1"],
.js-scenes .sc-friday[data-step="2"] .fr-q[data-answered-at="1"],
.js-scenes .sc-friday[data-step="2"] .fr-q[data-answered-at="2"],
.js-scenes .sc-friday[data-step="3"] .fr-q,
.js-scenes .sc-friday[data-step="4"] .fr-q {
  color: var(--fg-faint);
  border-color: transparent;
  text-decoration: line-through;
  text-decoration-color: rgba(var(--ml-rgb), 0.8);
  text-decoration-thickness: 1.5px;
  opacity: 0.7;
}
/* Without the engine every question is already answered — which is the final
   state, and the honest one for a reader without JavaScript. */
html:not(.js-scenes) .fr-q {
  color: var(--fg-faint);
  border-color: transparent;
  text-decoration: line-through;
  text-decoration-color: rgba(var(--ml-rgb), 0.8);
  text-decoration-thickness: 1.5px;
  opacity: 0.7;
}

/* ⚠️ stretch, not start: the event log is five rows plus an incident card, the
   fact panel is six lines — left is visibly taller, and with `start` the right
   panel stopped in mid-air with a hand's width of empty stage under it. Equal
   heights put the slack inside the card, where it reads as breathing room. */
.fr-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: stretch; }
.fr-side {
  padding: 16px 18px;
  border: 1px solid var(--ui-border);
  border-top: 3px solid var(--c);
  border-radius: var(--ui-radius);
  background: var(--ui-card-alt);
}
.fr-side-head { display: grid; gap: 2px; margin-bottom: 12px; }
.fr-side-head b { font-size: 14.5px; font-weight: 700; color: var(--ui-text); }
.fr-side-head span { font-size: 11.5px; font-family: var(--font-mono); color: var(--ui-faint); }
.fr-alert { margin-top: 12px; }

.fr-facts { display: grid; gap: 7px; }
.fr-facts div { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; }
.fr-facts span { color: var(--ui-faint); flex: 0 0 auto; }
.fr-facts b {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--ui-text);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
}
.fr-open {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--ui-border);
  font-size: 12.5px;
  color: var(--mm);
}
.fr-open svg { width: 14px; height: 14px; fill: currentColor; flex: 0 0 auto; }

/* ── The staging ──────────────────────────────────────────────────────────
   Both halves are present in the HTML from the start — they only fade up, so
   the stage never changes height while it is pinned and fit() has one number
   to work with instead of five. */
.js-scenes .sc-friday .fr-side,
.js-scenes .sc-friday .fr-alert { opacity: 0; transition: opacity 0.45s ease; }
.js-scenes .sc-friday .fr-side.is-now,
.js-scenes .sc-friday .fr-side.is-past,
.js-scenes .sc-friday .fr-alert.is-now,
.js-scenes .sc-friday .fr-alert.is-past { opacity: 1; }
/* The log rows arrive one at a time, on monolog's clock, not the reader's. */
.js-scenes .sc-friday .ui-log-row { opacity: 0; transform: translateY(4px); transition: opacity 0.3s ease, transform 0.3s ease; }
.js-scenes .sc-friday[data-step="1"] .ui-log-row,
.js-scenes .sc-friday[data-step="2"] .ui-log-row,
.js-scenes .sc-friday[data-step="3"] .ui-log-row,
.js-scenes .sc-friday[data-step="4"] .ui-log-row { opacity: 1; transform: none; }
.js-scenes .sc-friday[data-step="1"] .ui-log-row:nth-child(n+3) { opacity: 0.35; }

.fr-close { margin-top: 34px; text-align: center; }
.fr-close .claim {
  border-left: 0;
  padding-left: 0;
  margin: 0 auto;
  max-width: 30ch;
  font-size: clamp(22px, 2.8vw, 32px);
}
.fr-close-sub { max-width: 60ch; margin: 16px auto 0; font-size: clamp(15px, 1.4vw, 18px); color: var(--fg-dim); }

@media (max-width: 860px) {
  .fr-split { grid-template-columns: 1fr; }
}

/* ── 07 close: the claim, the loop, the joke ──────────────────────────────
   Outside the pinned stage: a line inside a scene the reader has already
   scrolled past is a line nobody reads twice. The loop line is load-bearing —
   it carries the argument the retired "Update" stage of the ring used to make
   (sec-reality.mjs explains why it may not be dropped). */
.rl-close { margin-top: 34px; text-align: center; }
.rl-close .claim {
  border-left: 0;
  padding-left: 0;
  margin: 0 auto;
  max-width: 26ch;
  font-size: clamp(23px, 3vw, 34px);
}
.rl-loop {
  max-width: 56ch;
  margin: 16px auto 0;
  font-size: clamp(16px, 1.5vw, 19px);
  font-weight: 600;
  color: var(--mm);
}

/* ── 06 The system becomes alive ──────────────────────────────────────────
   Shares the look of the product page's device browser (.hd-*, widened selector
   lists further down) but NOT its affordance: on the start page these cards are
   plain <div>s, because home.js wires a click only for [data-scene-replay] and a
   card that invites a click it cannot answer is worse than a picture. Phase 2
   turns them into buttons; every hook is already in the markup (sec-alive.mjs).
   ⚠️ Never fold .al-card into .hd-card: that one is shared with monolog.html
   and carries cursor:pointer plus a hover lift, which is exactly what must not
   come along. */
.al-card { border-bottom-width: 3px; }
.al-claim { margin-top: 26px; }

/* ── 05 Handover, the flat extras ─────────────────────────────────────────── */
.hv-keeps { margin-top: 20px; display: grid; gap: 10px; }
.hv-keeps-tag { font-size: 12px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--fg-faint); }
.hv-keeps ul { display: flex; flex-wrap: wrap; gap: 10px; margin: 0; padding: 0; list-style: none; }
.hv-keeps li {
  padding: 9px 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg-2);
  font-size: 14px;
  font-weight: 600;
  color: var(--fg);
}
.hv-claim { margin-top: 26px; }

/* ── 08 Systems change ────────────────────────────────────────────────────
   A log of an installation's life. Same row component as the Friday scene, one
   column wider: elapsed time ("+7 months") does not fit a clock's 62px, and a
   truncated "+3 mont…" reads as a bug. */
.ch { padding: 20px 18px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }
.ch-log { margin-top: 12px; }
.ch-row { grid-template-columns: 92px 1fr; align-items: start; padding: 10px 12px; }
.ch-row time { padding-top: 2px; font-variant-numeric: tabular-nums; }
.ch-what { display: grid; gap: 2px; }
.ch-what span { font-size: 12.5px; color: var(--fg-faint); line-height: 1.45; }

/* The division of labour, stated as a division of memory. */
.ch-split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 18px; }
.ch-side {
  display: grid;
  gap: 3px;
  padding: 16px 18px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--c);
  border-radius: var(--radius);
  background: var(--bg-2);
}
.ch-side b { font-family: 'Poppins', 'Manrope', system-ui, sans-serif; font-size: 17px; font-weight: 600; }
.ch-side b .thin { font-weight: 200; }
.ch-side span { font-size: 14px; color: var(--fg-dim); }
.ch-claim { margin-top: 26px; }

@media (max-width: 760px) {
  .ch-split { grid-template-columns: 1fr; }
  .ch-row { grid-template-columns: 76px 1fr; }
}

/* ── 11 Platform message ──────────────────────────────────────────────────
   The close, centred: it answers the hero rather than opening a new argument,
   so it is set as a statement and not as the head of another block of copy. */
.pf-head { text-align: center; max-width: 74ch; margin-left: auto; margin-right: auto; }
.pf-head .lead { margin-left: auto; margin-right: auto; }
/* .claim carries a 3px left border everywhere else on the site; centred, that
   border reads as a stray mark, so this one wears its emphasis as size. */
.pf-claim {
  border-left: 0;
  padding-left: 0;
  margin: 22px auto 0;
  max-width: none;
  font-size: clamp(24px, 3.2vw, 38px);
  text-align: center;
}

/* ── 02 Two products, one system ──────────────────────────────────────────
   Two panels and, spanning both, the claim that makes them one product family.
   ⚠️ NOT built on .door: those are links and carry a hover lift and a chevron.
   These panels are an introduction, and an element that looks clickable without
   being clickable is worse than a plain one (sec-products.mjs explains why). */
.pr { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; align-items: start; }
.pr-card {
  border: 1px solid var(--line);
  border-top: 3px solid var(--c);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 24px 22px 22px;
}
.pr-name {
  font-family: 'Poppins', 'Manrope', system-ui, sans-serif;
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.pr-name .thin { font-weight: 200; }
/* The one sentence a reader should still have an hour later, so it is set as a
   claim and not as body copy. */
.pr-claim {
  margin: 10px 0 12px;
  font-size: clamp(19px, 1.9vw, 23px);
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--c);
}
.pr-p { margin: 0 0 16px; font-size: 15px; line-height: 1.6; color: var(--fg-dim); }
.pr-chips { display: flex; flex-wrap: wrap; gap: 7px; }

/* The bridge. Centred and full width, because the claim is about the pair. */
.pr-shared {
  margin-top: 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-2);
  padding: 26px 24px;
  text-align: center;
}
.pr-shared-tag {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  /* Neither product owns this line — it is the thing they share. */
  color: var(--fg-faint);
}
.pr-shared-p { max-width: 64ch; margin: 10px auto 16px; font-size: clamp(16px, 1.5vw, 19px); line-height: 1.55; color: var(--fg); }
.pr-shared-counts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.pr-suite { margin: 16px 0 0; font-size: 14px; color: var(--fg-faint); }

@media (max-width: 860px) {
  .pr { grid-template-columns: 1fr; }
}

.doors { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
.door {
  display: block;
  text-decoration: none;
  border: 1px solid var(--line);
  border-top: 2px solid var(--c);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 26px 24px;
  transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
}
.door:hover { transform: translateY(-2px); background: var(--panel-2); }
.door-name {
  font-family: 'Poppins', 'Manrope', system-ui, sans-serif;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.door-name .thin { font-weight: 200; }
.door-q { margin: 6px 0 12px; font-family: var(--font-mono); font-size: 13px; color: var(--c); }
.door p { font-size: 14.5px; color: var(--fg-dim); margin: 0 0 14px; }
.door-more { font-size: 14px; font-weight: 700; color: var(--c); display: inline-flex; align-items: center; gap: 7px; }
@media (max-width: 760px) { .doors { grid-template-columns: 1fr; } }

/* ── Reduced motion ───────────────────────────────────────────────────────
   boot.js already withholds `.js-scenes`, so nothing below `.js-scenes` can
   run. This block covers the pieces that animate without it. */
@media (prefers-reduced-motion: reduce) {
  .ui-dot.is-online, .ui-dot.is-warning, .ui-dot.is-offline { animation: none; }
  .xf-step, .pv-cell, .pv-op, .pv-verdict, .pv-proposal, .door { transition: none; }
}

/* ── Animated hero of the MonoLog product page ────────────────────────────
   Lives here rather than in a file of its own because it is built from the same
   UI kit as everything above, and monolog.html now loads this stylesheet for
   exactly that reason (see build-hero-demo.mjs and sync-static-chrome.mjs).

   A still screenshot says "here is a dashboard". A room being clicked through
   says what the thing is FOR — so the cards select themselves in turn and the
   detail panel follows. Time-driven: browsing is a rhythm, not a scroll
   position. */
.hero-demo { margin-top: 48px; }
.hd {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 22px 22px 20px;
}
.hd-grid, .al-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--gap-card, 14px);
}
/* The cards are real buttons: the scene selects them on its own, and a reader
   who reaches for one gets the same thing rather than a dead picture. */
.hd-card {
  font: inherit;
  text-align: left;
  cursor: pointer;
  border: 1px solid var(--ui-border);
  border-bottom-width: 3px;
  transition: border-color 0.25s ease, background 0.25s ease, transform 0.25s ease;
}
.hd-card:hover { background: var(--ui-card); transform: translateY(-1px); }
.hd-card .ui-status, .al-card .ui-status { margin-top: 4px; }
.js-scenes .hd .hd-card, .js-scenes .al .al-card { opacity: 0.62; }
.js-scenes .hd .hd-card.is-now,
.js-scenes .al .al-card.is-now {
  opacity: 1;
  background: var(--ui-card);
  box-shadow: var(--ui-shadow), 0 0 0 2px rgba(var(--ml-rgb), 0.45);
}

.hd-panel, .al-panel {
  position: relative;
  margin-top: 16px;
  padding: 16px 18px;
  border: 1px solid var(--ui-border);
  border-radius: var(--ui-radius);
  background: var(--ui-card-alt);
  min-height: 172px;
}
.hd-detail, .al-detail { transition: opacity 0.3s ease; }
.hd-detail-head, .al-detail-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; }
.hd-detail-head b, .al-detail-head b { font-size: 15px; font-weight: 700; color: var(--ui-text); }
.hd-detail .ui-telemetry, .al-detail .ui-telemetry { border-top: 0; margin-top: 0; padding-top: 0; }
.hd-detail .ui-telemetry span, .al-detail .ui-telemetry span { color: var(--ui-faint); }
.hd-detail .ui-device-card-foot, .al-detail .ui-device-card-foot { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--ui-border); }

/* One detail at a time. Without the engine the first one stands — the panel is
   never empty, and the page still explains itself with JavaScript switched
   off. The rest are stacked out of flow so the panel does not change height as
   the scene moves through devices with different numbers of telemetry rows. */
.hd-detail, .al-detail { display: none; }
/* ⚠️ Das war :first-of-type und hat NIE gegriffen: "of-type" meint den
   Elementtyp, nicht die Klasse — und das erste div in diesem Feld ist die
   Ueberschrift, nicht die erste Karte. Ohne JavaScript und bei
   prefers-reduced-motion stand das Detailfeld deshalb leer da, also genau in
   den zwei Faellen, in denen das HTML den Endzustand allein tragen muss.
   data-at="0" ist dieselbe Marke, die die Engine ohnehin setzt. */
.hd-detail[data-at="0"], .al-detail[data-at="0"] { display: block; }
/* ⚠️ Nur wenn wirklich ein Schritt laeuft — sonst steht das Feld leer, und zwar
   nicht nur kurz beim Laden: reset() nimmt data-step wieder weg, sobald die
   Szene das Bild verlaesst. Ohne [data-step] waere der Hero jedes Mal leer,
   wenn der Leser nach oben zurueckkommt. */
.js-scenes .hd[data-step] .hd-detail[data-at="0"]:not(.is-now),
.js-scenes .al[data-step] .al-detail[data-at="0"]:not(.is-now) { display: none; }
/* ⚠️ :not(.is-now) ist nicht Zierde. [data-step] hebt die Spezifitaet ueber die
   is-now-Regel darunter — ohne den Ausschluss gewinnt display:none genau dann,
   wenn Schritt 0 an der Reihe ist, und die erste Karte fehlt im Durchlauf. */
.js-scenes .hd .hd-detail.is-now,
.js-scenes .al .al-detail.is-now { display: block; }

.hd-note, .al-note {
  margin: 16px 0 0;
  font-size: 13.5px;
  color: var(--fg-faint);
  max-width: 82ch;
}

@media (max-width: 700px) {
  .hero-demo { margin-top: 34px; }
  .hd { padding: 16px 14px 14px; }
  .hd-panel, .al-panel { min-height: 200px; }
}

