/*
 * Copyright 2026 Jason Figge
 *
 * This file is part of Chip Hippo.
 *
 * Chip Hippo is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Chip Hippo is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with Chip Hippo. If not, see <https://www.gnu.org/licenses/>.
 */

/* app.css — the app shell: header bar above the full-bleed desk viewport.
   Desk behaviour (pan/zoom camera, boards) arrives with Features 10–30. */

#app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

/* ── Header ──────────────────────────────────────────────────────────────── */
.app-header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  height: var(--header-height);
  padding: 0 var(--space-4);
  background: var(--color-mantle);
  border-bottom: 1px solid var(--color-crust);
  flex: 0 0 auto;
}

.app-header-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The icon is a bare button (opens the About dialog) — no default chrome. */
.app-header-icon-btn {
  display: flex;
  padding: 2px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
  color: inherit;
}

.app-header-icon-btn:hover {
  background: var(--color-surface-1);
}

.app-header-icon-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.app-header-icon {
  display: block;
  user-select: none;
}

.app-logo {
  font-size: var(--font-size-xl);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* Toolbar slot — desk tools mount here. Flexes to fill the header so the
   meta block stays pinned to the right edge. */
.app-header-toolbar {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--space-4); /* between pills: File · tools · transport, no rules */
  min-width: 0;
}

.toolbar-btn--active {
  background: var(--color-surface-1);
  border-color: var(--color-accent);
}

/* ── Toolbar pill ─────────────────────────────────────────────────────────
   The toolbar's grouping shape, used by the desk tools (Wire / Bus / Fade /
   Probe / Fit) and by File (New / Open / Open Recent / Save / Save As / BOM).
   Whatever it holds reads as ONE control: the PILL carries the only border and
   the
   only background, and its segments are separated by spacing rather than by
   borders of their own. Each segment is still a plain button — only the
   chrome is shared. */
.toolbar-pill {
  flex: 0 0 auto; /* a squeezed header wrapped "Stop" under its glyph */
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--toolbar-height);
  padding: 0 var(--space-1);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
}

.toolbar-pill-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--segment-height);
  padding: 0 var(--space-2);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text);
  font-size: var(--font-size-sm);
  white-space: nowrap;
  cursor: pointer;
}

/* A labelled segment sharing a pill with icon segments (Wire, Bus): padded
   so the space between any two neighbours' ink is the icons' own — 4 px of
   pill gap plus 4 px either side, the margin a 16 px glyph has in its square.
   The roomier default left Wire → Bus at 20 px and Bus → Auto-route at 16. */
.toolbar-pill-btn--tool {
  padding: 0 var(--space-1);
}

/* An icon-only segment (Fade / Probe / Analyzer / Fit, and every File action)
   is square and de-emphasized. */
.toolbar-pill-btn--icon {
  justify-content: center;
  width: calc(var(--segment-height) + 2px);
  padding: 0;
  color: var(--color-subtext);
}

.toolbar-pill-btn:enabled:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* Armed / on: the segment FILLS, since it has no border to accent, and its
   glyph (or label) turns the app icon's green. The fill is hover's own
   surface-1; the green is what says "on", so a toggled segment still reads
   apart from one the pointer is merely over — and keeps it under hover, which
   would otherwise wash it back to text. Beats the shared .toolbar-btn--active
   rule (which colours a border this button doesn't have) on specificity, so
   one class still drives every toolbar button's state. */
.toolbar-pill-btn.toolbar-btn--active,
.toolbar-pill-btn.toolbar-btn--active:enabled:hover {
  background: var(--color-surface-1);
  color: var(--color-brand);
}

.toolbar-pill-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

/* A hidden segment must COLLAPSE — the pill's flex row would otherwise still
   lay it out. This is what lets the transport pill be one Run button while the
   circuit is stopped and a four-segment transport the moment it runs. */
.toolbar-pill-btn[hidden] {
  display: none;
}

/* ── Transport pill (Feature 90/100) ─────────────────────────────────────
   Run/Stop · Pause · Step · speed, grouped in the same pill the desk tools
   use, set apart from them by the toolbar's own gap. */

/* Run/Stop keeps its signal — green to run, red while running — as COLOR
   alone: the pill carries the only border, so the segment has none to accent.
   The hover pairs restate it, since .toolbar-pill-btn:enabled:hover would
   otherwise out-specify the base colour and wash the segment back to text. */
.toolbar-pill-btn--run,
.toolbar-pill-btn--run:enabled:hover {
  font-weight: 600;
  color: var(--color-success);
}

.toolbar-pill-btn--run.toolbar-btn--running,
.toolbar-pill-btn--run.toolbar-btn--running:enabled:hover {
  color: var(--color-danger);
}

/* The speed button while the circuit cannot keep up with the speed asked: it
   shows the speed achieved beside it, in amber (app.js showSpeed). */
.toolbar-pill-btn--behind,
.toolbar-pill-btn--behind:enabled:hover {
  color: var(--color-warning);
}

/* The active wire color, beside the Wire label. Unlike the Bus badge this
   readout is also the PICKER for what it shows — clicking it opens the eight
   swatches (components/wire-color-dot.js) — so it gets a hover ring of its
   own: the segment's hover fill cannot say "this 12 px circle is a separate
   target". A box-shadow costs no layout, so the pill never shifts. */
.wire-swatch-dot {
  width: 12px;
  height: 12px;
  border: 1px solid var(--color-overlay);
  border-radius: 50%;
  background: var(--wire-color);
}

.wire-swatch-dot:hover {
  box-shadow: 0 0 0 2px var(--color-accent);
}

/* While the circuit runs the Wire segment is disabled — but a disabled button
   only suppresses its OWN activation, not clicks on what is inside it, so the
   dot has to be taken out of the hit test explicitly (which drops its hover
   ring with it). wire-color-dot.js guards the same case in JS, where the test
   can see it; this is what makes the dead control look dead. */
.toolbar-pill-btn:disabled .wire-swatch-dot {
  pointer-events: none;
}

/* The active bus width, displayed beside the Bus label (the digit keys change
   it). Like the Wire dot this readout is also the PICKER for what it shows
   (components/bus-width-badge.js), so it gets a hover ring of its own — the
   segment's hover fill cannot say "this circle is a separate target" — and it
   is CIRCLED, which is the shape the popover's options repeat. A box-shadow
   costs no layout, so the pill never shifts. */
.bus-width-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--font-size) + 5px);
  height: calc(var(--font-size) + 5px);
  padding: 0 3px;
  box-sizing: border-box;
  background: var(--color-surface-1);
  border: 1px solid var(--color-overlay);
  border-radius: 4px;
  color: var(--color-subtext);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

.bus-width-badge:hover {
  box-shadow: 0 0 0 2px var(--color-accent);
}

/* Same story as the Wire dot above: a disabled Bus segment does not make the
   badge inside it inert, so it leaves the hit test explicitly (and its hover
   ring with it). bus-width-badge.js guards the same case in JS. */
.toolbar-pill-btn:disabled .bus-width-badge {
  pointer-events: none;
}

/* The width picker the badge opens: one circled number per preset, in a row —
   the badge's own glyph, repeated at a size worth clicking. */
.bus-width-options {
  display: flex;
  gap: var(--space-2);
}

.bus-width-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--font-size) + 13px);
  height: calc(var(--font-size) + 13px);
  padding: 0 4px;
  box-sizing: border-box;
  border: 1px solid var(--color-overlay);
  border-radius: 4px;
  background: var(--color-surface-1);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  box-shadow: 0 0 0 2px transparent;
  transition:
    box-shadow 0.15s ease,
    transform 0.15s ease;
}

.bus-width-option:hover {
  transform: scale(1.1);
}

.bus-width-option--selected {
  border-color: var(--color-accent);
  color: var(--color-text);
  box-shadow: 0 0 0 2px var(--color-accent);
}

.bus-width-option:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ── Main row: palette beside the desk ───────────────────────────────────── */
/* The parts tray (or, shut, the rail it shuts down to) then the desk column —
   both layout columns, so nothing floats over the desk's left edge. */
/* ── What's-new banner (components/notice-banner.js) ─────────────────────── */
/* A bar IN FLOW between the toolbar and the main row, so it covers nothing —
   the desk, tray and tabs simply start below it, and get the room back when
   its ✕ is clicked. Info-blue edge, as an info toast has. */
.notice-banner {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4);
  background: var(--color-surface-0);
  border-bottom: 1px solid var(--color-crust);
  border-left: 4px solid var(--color-info);
  color: var(--color-text);
  font-size: var(--font-size-sm);
}

.notice-banner-body {
  flex: 1 1 auto;
  min-width: 0;
}

.notice-banner-title {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size);
  font-weight: 600;
}

/* The lines flow into as many columns as the window has room for — never
   more than three (each at least 30 % wide), so six lines make two even rows
   on a desktop-sized window rather than a ragged four and two. */
.notice-banner-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(22em, 30%), 1fr));
  gap: 2px var(--space-4);
  margin: 0;
  padding-left: var(--space-4);
}

/* Each line's bullet hangs outside it, so it needs room of its own — or a
   column's bullet sits flush against the line to its left. */
.notice-banner-list li {
  margin-left: var(--space-3);
}

.notice-banner-close {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  /* One line of text plus constant chrome, like the zoom cluster's buttons. */
  width: calc(var(--font-size) + 11px);
  height: calc(var(--font-size) + 11px);
  padding: 0;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-muted);
  font-size: var(--font-size);
  cursor: pointer;
}

.notice-banner-close:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.app-main {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

/* The desk column: the tab strip stacked over whichever surface is showing.
   It is a row item of .app-main, so it starts to the right of the parts
   palette — the tabs line up with the desk, not with the palette. */
/* The desk padlock's own box, declared here because the desktop tab strip has
   to stop short of it (both live over the desk's top edge) — and, for the same
   reason, the zoom cluster's, because the SIGNAL RAIL (Feature 370) runs down
   the right edge BETWEEN the two and has to clear both. Derived from the very
   terms .desk-zoom sizes its buttons from (a box is one line of text plus
   constant chrome), and consumed by .desk-zoom itself, so the two cannot
   drift apart. */
.app-stage {
  --desk-lock-size: 24px;
  --desk-zoom-height: calc(var(--font-size) + 11px + 2 * var(--space-1) + 2px);
  /* …and its WIDTH, for the serial integration's activity lamps, which sit
     immediately to its left: two square buttons and the readout's floor, the
     two gaps between them, the padding and the border. Consumed by .desk-zoom
     too, so the lamps can never drift onto it. */
  --desk-zoom-width: calc(
    2 * (var(--font-size) + 11px) + 48px + 4 * var(--space-1) + 2px
  );
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}

/* ── Desktop tabs (Feature 240) ──────────────────────────────────────────── */
/* One tab per desktop in the open project, in ONE floating pill over the desk
   — the toolbar's grouping shape (see .toolbar-pill), so the app has a single
   vocabulary for "these controls are one thing". The pill carries the only
   background; a tab is a borderless segment that FILLS when it is the active
   desktop. It is sized to its content and inset from the corner, so the dot
   grid runs behind it unbroken and every click outside it still reaches the
   desk. Hidden entirely when no project is open, so the shell is unchanged. */
.project-tabs {
  position: absolute;
  top: var(--space-2);
  left: var(--space-3);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  /* Stop clear of the padlock in the opposite corner (see .desk-lock) — with a
     transparent background it has nothing to hide an overlap behind. */
  max-width: calc(
    100% - 2 * var(--space-3) - var(--desk-lock-size) - var(--space-2)
  );
  padding: var(--space-1);
  background: var(--color-mantle);
  border: 1px solid var(--color-crust);
  border-radius: var(--radius-md);
  overflow-x: auto;
  scrollbar-width: thin;
}

/* A desk switch waiting on its Stop (kept ROM edits being written): what is
   clicked meanwhile waits its turn. */
.project-tabs--busy,
.project-tabs--busy * {
  cursor: progress;
}

.project-tab,
.project-tab-add {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: calc(var(--font-size) + 11px);
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  font: inherit;
  white-space: nowrap;
  cursor: pointer;
}

.project-tab:hover,
.project-tab-add:hover {
  color: var(--color-text);
  background: var(--color-surface-0);
}

/* The desktop on the desk: a filled chip, the way an armed pill segment fills. */
.project-tab--active,
.project-tab--active:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* There is no per-tab dirty marker: the PROJECT is the document, so a desktop
   cannot be saved on its own and a dot no action could clear would be a lie.
   The one marker is the window title's. */

.project-tab-add {
  color: var(--color-overlay);
}

/* ── Parts palette (Feature 40) ──────────────────────────────────────────── */
/* The width here is only the fallback: the renderer applies the user's own
   (settings.paletteWidth) as an inline style. `max-width` is the same ceiling
   the analyzer's `max-height: 50vh` is — it keeps a tray sized on a wide
   display from swallowing the desk when the window is later made narrow. */
.palette-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 232px;
  max-width: 50vw;
  flex: 0 0 auto;
  background: var(--color-mantle);
  border-right: 1px solid var(--color-crust);
  min-height: 0;
}

.palette-panel[hidden] {
  display: none;
}

/* The draggable right edge — the analyzer's resize seam stood on end (same
   grip, same hover reveal, `ew-resize` instead of `ns-`). It straddles the
   border rather than sitting inside the panel, so it never covers the parts
   list's scrollbar; the 3 px reaching over the desk is grab room, and the
   desk beneath is only ever panned, never dropped on, at that edge. */
.palette-resize {
  position: absolute;
  z-index: 3;
  top: 0;
  right: -3px;
  width: 6px;
  height: 100%;
  cursor: ew-resize;
  touch-action: none;
}

.palette-resize::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3px;
  height: 44px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-sm);
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity 0.12s ease;
}

.palette-resize:hover::after,
.palette-resize--active::after {
  opacity: 1;
}

.palette-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
  border-bottom: 1px solid var(--color-crust);
}

/* The tray's own close control, in the header's top-right corner — the tray
   has no toolbar button. Clicking it shuts the tray down to its rail, whose
   own chevron (below) reopens it from the same line. */
.palette-collapse {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.palette-collapse:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* The section icons' size (palette-icons.js), ONE value for both places they
   show — the open tray's headers and the shut tray's rail — so shutting the
   tray never resizes one. 30% over one line of the base text, so they follow
   the Editor font size. */
.palette-panel,
.palette-rail {
  --palette-icon: calc(1.3 * var(--font-size));
}

/* The rail: what the tray shuts down TO — the tray's own surface left showing
   along the desk's left edge, one icon wide (palette-rail.js). A layout column
   like the tray, so the desk and its tab strip start to the right of it. Every
   box below is the icon plus constant chrome, so the strip widens with its
   icons rather than clipping them. */
.palette-rail {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  min-height: 0;
  background: var(--color-mantle);
  border-right: 1px solid var(--color-crust);
}

.palette-rail[hidden] {
  display: none;
}

/* The reopen chevron's row IS the tray header's, shut: the same padding, a
   control-high button and the same rule under it, so the chevron and the rule
   stay exactly where they were and the tray reads as sliding into the wall. */
.palette-rail-head {
  display: flex;
  justify-content: center;
  padding: var(--space-2) var(--space-1);
  border-bottom: 1px solid var(--color-crust);
}

/* Each icon sits on the ROW its section's header occupies in a shut tray, so
   shutting the tray leaves every icon exactly where its label was. That takes
   the list's own top padding (.palette-list), each header's own margins — in a
   BLOCK column, not a flex gap, so they collapse between neighbours exactly as
   the headers' do — and a row one line of that header's text tall plus its
   padding. `palette-panel.test.js` holds these to the header rules.
   Level VERTICALLY only, and deliberately: in the open tray the caret comes
   first, so each icon sits a caret's width further right there, and opening
   the tray reads as the drawer stretching out. Do not widen the rail to
   "fix" it. */
.palette-rail-sections {
  min-height: 0;
  padding: var(--space-2) var(--space-1);
  overflow-y: auto;
}

.palette-rail-toggle,
.palette-rail-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: calc(var(--palette-icon) + 2 * var(--space-2));
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.palette-rail-toggle {
  height: var(--control-height);
}

/* A folder header's row (.palette-folder and its peers). */
.palette-rail-btn {
  height: calc(var(--font-size-sm) * var(--line-height) + 2 * var(--space-1));
  margin: var(--space-3) auto var(--space-1);
}

/* BOARDS leads the list, so its header carries no top margin. */
.palette-rail-btn:first-child {
  margin-top: 0;
}

/* A GROUP header's row (.palette-group — Memory), whose text is a size
   smaller. */
.palette-rail-btn--group {
  height: calc(var(--font-size-xs) * var(--line-height) + 2 * var(--space-1));
}

.palette-rail-toggle:hover,
.palette-rail-btn:hover {
  background: var(--color-surface-1);
}

.palette-rail-toggle:hover {
  color: var(--color-text);
}

.palette-rail-toggle:focus-visible,
.palette-rail-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* The section icons, in the app icon's green (`--color-brand`) wherever they
   show — a hover lifts the button or the header's text, never the tint.
   `block` drops the inline baseline gap so the glyph centres exactly. */
.palette-rail-btn svg,
.palette-section-icon svg {
  display: block;
  color: var(--color-brand);
  width: var(--palette-icon);
  height: var(--palette-icon);
}

/* A top-level header's icon, between the caret and the label. A ZERO-HIGH
   flex item whose glyph overflows it evenly: the icon is taller than a GROUP
   header's line of text (Memory's), and a header that grew to fit it would
   no longer be the row the rail's icons are measured to sit on. */
.palette-section-icon {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  height: 0;
}

/* Takes the header row's remaining width, beside the collapse chevron. */
.palette-filter {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--control-height);
  padding: 0 var(--space-2);
  background: var(--color-base);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font: inherit;
}

.palette-list {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-2);
}

.palette-group {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  margin: var(--space-3) 0 var(--space-1);
  padding: var(--space-1) 0;
  border: none;
  background: transparent;
  text-align: left;
  cursor: pointer;
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

.palette-group:first-child {
  margin-top: 0;
}

.palette-group:hover {
  color: var(--color-text);
}

/* Disclosure caret — points down when open, right when collapsed. The Mock
   panel's (its Inputs, its Faults) is the same caret. */
.palette-group-caret,
.mock-caret {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform 0.12s ease;
}

.palette-group--collapsed .palette-group-caret,
.palette-folder--collapsed .palette-group-caret,
.palette-boards-folder--collapsed .palette-group-caret,
.palette-annotations-folder--collapsed .palette-group-caret,
.palette-signals-folder--collapsed .palette-group-caret {
  transform: rotate(-90deg);
}

.palette-group-items[hidden] {
  display: none;
}

/* Top-level folder headers — the "Chips"/"Components" wrapper, and the
   ANNOTATIONS and SIGNALS sections pinned at the bottom. One level above a
   group: bolder, mixed-case, stronger colour.
   ONE rule for all three, not a copy each. The annotations block used to be a
   byte-identical duplicate of this one, and the cost showed up the moment a
   third section arrived: SIGNALS was given a class of its own with no rule
   behind it, so it fell back to the UA's default button chrome (a grey filled
   box) and its caret never rotated. `.palette-boards-folder` stays separate
   because it genuinely differs — it is pinned at the very top, so it carries
   no top margin. */
.palette-folder,
.palette-annotations-folder,
.palette-signals-folder {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  margin: var(--space-3) 0 var(--space-1);
  padding: var(--space-1) 0;
  border: none;
  background: transparent;
  text-align: left;
  cursor: pointer;
  font-size: var(--font-size-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-muted);
}

.palette-folder:first-child {
  margin-top: 0;
}

.palette-folder:hover,
.palette-annotations-folder:hover,
.palette-signals-folder:hover {
  color: var(--color-text);
}

/* Chip groups sit indented one level under the folder. */
.palette-folder-groups {
  padding-left: var(--space-3);
}

.palette-folder-groups[hidden] {
  display: none;
}

/* Each leaf folder's list is ONE two-column grid — the name column as wide as
   that folder's longest ID — and each row joins it as a subgrid, so every
   description in a folder starts at the same x. Subgrid, not
   `display: contents`: the row must keep its own box for the hover wash, the
   focus ring and its place in the accessibility tree. The column gap is stated
   here once; the rows inherit it. */
.palette-group-items,
.palette-boards-items {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--space-2);
}

.palette-item,
.palette-board-item,
.palette-annotation-item,
.palette-signal-item,
.palette-custom-new {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
  width: 100%;
  text-align: left;
  padding: var(--space-2);
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--color-text);
}

.palette-item:hover,
.palette-board-item:hover,
.palette-annotation-item:hover,
.palette-signal-item:enabled:hover,
.palette-custom-new:hover {
  background: var(--color-surface-0);
}

.palette-annotation-item,
.palette-signal-item,
.palette-custom-new {
  cursor: pointer;
}

/* A designed chip's part number wears the plastic and the `</>` it has on the
   desk (chip-view.js), so a custom chip reads as one wherever it appears. */
.palette-item-id--custom {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0 0.4em;
  border-radius: var(--radius-sm);
  background: var(--color-chip-custom-body);
  color: var(--color-chip-label);
}

.palette-custom-glyph {
  display: inline-flex;
  color: var(--color-chip-custom-glyph);
}

/* Every wire colour taken — the row stays, saying why (see setSignalsFull). */
.palette-signal-item:disabled {
  cursor: default;
  color: var(--color-muted);
}

/* Board selector pinned at the top of the palette (Full/Half/Tiny + strips). */
.palette-board-item {
  cursor: pointer;
}

/* The Boards header is a foldable section (like the Chips folder), pinned at
   the top of the palette. */
.palette-boards-folder {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  width: 100%;
  margin: 0 0 var(--space-1);
  padding: var(--space-1) 0;
  border: none;
  background: transparent;
  text-align: left;
  cursor: pointer;
  font-size: var(--font-size-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-muted);
}

.palette-boards-folder:hover {
  color: var(--color-text);
}

.palette-boards-items[hidden] {
  display: none;
}

/* An ID never breaks (`sw-slide` used to wrap at its hyphen). `start`, not
   the grid's stretch, so a custom chip's plastic badge stays its own width. */
.palette-item-id {
  justify-self: start;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-weight: 600;
}

/* The description wraps inside its column; a word too long for it (a narrow
   tray beside `cap-electrolytic`) breaks rather than scrolling the list
   sideways. */
.palette-item-title {
  min-width: 0;
  overflow-wrap: break-word;
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.palette-empty {
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  padding: var(--space-2);
}

/* Right-aligned header action panel (the system/settings icon). */
.header-icon-panel {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Article embeds keep the simulator editable while removing project-level
   file chrome. Reset and Open full workbench own the lesson lifecycle. */
[data-chiphippo-mode="embed"] .toolbar-pill--file,
[data-chiphippo-mode="embed"] .header-icon-btn {
  display: none;
}

.embed-hide-tabs .project-tabs,
.embed-hide-palette .palette-panel,
.embed-hide-palette .palette-rail {
  display: none;
}

.embed-action {
  display: inline-flex;
  align-items: center;
  min-height: var(--toolbar-height);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-0);
  color: var(--color-text);
  font: inherit;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.embed-action:hover {
  background: var(--color-surface-1);
}

.embed-action--primary {
  border-color: var(--color-accent);
  color: var(--color-brand);
}

@media (max-width: 850px) {
  [data-chiphippo-mode="embed"] .app-header-brand {
    display: none;
  }

  /* The meter is an article interaction, not project chrome. Keep it beside
     Run even when the rest of the wide workbench tool strip is collapsed. */
  [data-chiphippo-mode="embed"]
    .toolbar-pill--tools
    > :not(.toolbar-meter):not(.toolbar-oscilloscope) {
    display: none;
  }
}

/* Bare icon buttons (ported from Rest Hippo). */
.icon-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.icon-btn:hover {
  background-color: var(--color-surface-1);
  color: var(--color-text);
}

.icon-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.header-icon-btn {
  width: var(--toolbar-height);
  height: var(--toolbar-height);
}

/* ── Desk viewport (Feature 10) ──────────────────────────────────────────── */
/* A fixed clipping frame over one .desk-surface child carrying the camera
   transform. The dot grid is a background tile on the viewport itself: its
   size/position are recomputed from the camera by DeskView (desk-geometry
   gridForCamera), so the grid is infinite for free. The dot is centered in
   the tile so grid points land exactly on world-unit multiples. */
.desk-viewport {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  background-color: var(--color-base);
  background-image: radial-gradient(
    circle,
    var(--color-desk-dot) 1px,
    transparent 1.5px
  );
  /* A standard pointer at rest — the cursor only changes once a drag begins
     (closed hand for a part/pan, crosshair for a marquee or a tool). */
  cursor: default;
  /* Every desk gesture is a pointer drag the app steers itself (pan, board,
     part, marquee, wire, bus). Without this the browser can decide, partway
     through one, that the movement was really a scroll/zoom it should own —
     and it takes the pointer by firing `pointercancel`, which every one of
     those gestures correctly treats as "revert, never commit". The drop just
     vanishes, intermittently and velocity-dependently, on exactly the
     trackpad/touch input most likely to produce it. */
  touch-action: none;
  /* And nothing in here is text anyone would want to select — but a drag that
     crosses a label selects it, which paints a highlight over the desk and
     leaves it there. It went unnoticed until the SIGNAL RAIL, because it is the
     first desk chrome carrying words: dragging a flag off it highlighted the
     button labels and the zoom readout on the way past. Set on the viewport
     rather than per-control so the next piece of chrome inherits the fix. */
  user-select: none;
}

.desk-viewport--panning {
  cursor: grabbing;
}

/* Space held: a drag anywhere pans, so the whole desk shows the open hand —
   forced on every descendant, since parts, wires and flags carry cursors of
   their own and none of them is what a press would grab now. */
.desk-viewport--pan-ready,
.desk-viewport--pan-ready * {
  cursor: grab !important;
}

.desk-viewport--pan-ready.desk-viewport--panning,
.desk-viewport--pan-ready.desk-viewport--panning * {
  cursor: grabbing !important;
}

/* Option/Alt held — pin-targeting mode: bodies are inert, so nothing on the
   desk offers a grab cursor; only the red ring says what a click would pick. */
.desk-viewport--pin-mode,
.desk-viewport--pin-mode * {
  cursor: default !important;
}

/* …until an Option/Alt drag that picked no lead pans the desk. */
.desk-viewport--pin-mode.desk-viewport--panning,
.desk-viewport--pin-mode.desk-viewport--panning * {
  cursor: grabbing !important;
}

/* Something grabbed and being moved: a board, part, brick, annotation, signal
   flag or Output/Input tag (`--dragging`), or a wire or bus end
   (`--wire-dragging`) — the closed hand, as a pan shows. It is forced on every
   descendant too, because a drag crosses items with a grab cursor of their own
   (flags, tags, wires, annotations; tags in adjacent holes overlap), and the
   OPEN hand reappearing over one would say nothing is held. `!important`
   because several of those rules out-rank a descendant selector. */
.desk-viewport--dragging,
.desk-viewport--dragging *,
.desk-viewport--wire-dragging,
.desk-viewport--wire-dragging * {
  cursor: grabbing !important;
}

/* The infinite world: children are absolutely positioned in world px
   (world units × PX_PER_UNIT). Pan/zoom only mutates this transform. */
.desk-surface {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  transform-origin: 0 0;
  will-change: transform;
}

.desk-viewport--placing {
  cursor: crosshair;
}

/* The rubber band (a drag from the background) — crosshair, the conventional
   marquee cursor. Boards and parts set no cursor of their own, so it inherits
   across the whole desk. */
.desk-viewport--selecting {
  cursor: crosshair;
}

.desk-viewport--probing {
  cursor: pointer;
}

.desk-viewport--metering {
  cursor: crosshair;
}

.desk-hint {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--color-muted);
  font-size: var(--font-size-lg);
  user-select: none;
  pointer-events: none;
}

/* The explicit display above would defeat the hidden attribute (app.js hides
   the hint once the desk has boards) — restore it. */
.desk-hint[hidden] {
  display: none;
}

/* ── Surface layers (Feature 30) ─────────────────────────────────────────── */
/* Zero-size anchors at the world origin; children position in world px.
   Order: boards under parts (40/60) under wires (50) under annotations under
   signal flags (370) under the interaction overlay (ghosts, hover ring,
   selection, tooltips). A flag sits above the wires because it is hardware
   plugged into the board, drawn over the jumper running past it. */
.layer-boards,
.layer-parts,
.layer-wires,
.layer-annotations,
.layer-signals,
.layer-overlay {
  position: absolute;
  top: 0;
  left: 0;
}

.layer-overlay {
  pointer-events: none; /* ghosts/rings/tooltips never intercept the desk */
}

/* The rubber band. Lives in the overlay layer, so it's pointer-inert and
   rides the camera transform with everything else. */
.marquee {
  position: absolute;
  border: 1px solid var(--color-accent);
  background: rgba(120, 170, 255, 0.12);
  border-radius: 2px;
}

/* ── Boards ──────────────────────────────────────────────────────────────── */
.board {
  position: absolute;
  border-radius: 6px; /* matches the SVG body corner for outline hugging */
}

.board > svg {
  display: block;
}

/* `.board--selected` carries no outline of its own: the highlighter is ONE
   path traced around the whole snapped set (see `.board-outline` below), so a
   selection spanning several flush strips reads as a single item. */

.board--dragging {
  opacity: 0.85;
}

/* The strips a grab will move, lit as ONE item: a wash rather than a border,
   so flush neighbours merge into a single block instead of drawing a seam
   down every join. Works for any snapped shape, not just a tidy stack. */
.board--drag-set::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--color-accent);
  opacity: 0.18;
  pointer-events: none;
}

/* The selection highlighter: the outer edge of every strip a grab would
   move, drawn in world px (so it scales with the camera, as a CSS outline
   inside the transformed surface did) and reddened on an illegal drop. */
.board-outline {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
  pointer-events: none;
}

.board-outline-path {
  fill: none;
  /* --color-selection is the "Selection border colour" setting; it falls back
     to the theme accent until the Settings dialog overrides it on :root. */
  stroke: var(--color-selection, var(--color-accent));
  stroke-width: 2;
}

.board-outline--illegal .board-outline-path {
  stroke: var(--color-danger);
  /* A red rim alone is lost beside the orange selection rims — the whole
     set a refused drop would put back is washed red as well. */
  fill: color-mix(in srgb, var(--color-danger) 25%, transparent);
}

/* SVG board parts — all colors from the theme tokens. */
.board-body {
  fill: var(--color-board-body);
  stroke: var(--color-board-edge);
  stroke-width: 0.15;
}

.board-trench {
  fill: var(--color-board-trench);
}

.board-hole {
  fill: var(--color-board-hole);
}

.board-rail-stripe--plus {
  fill: var(--color-rail-plus);
}

.board-rail-stripe--minus {
  fill: var(--color-rail-minus);
}

/* THE FIRST OF THE DESK'S OWN TYPE, AND NONE OF IT IS ON THE TYPE SCALE.
   Every `font-size` from here down through `.bus-band-label` is stated in SVG
   USER UNITS — one unit is one breadboard pitch — so it is markings PRINTED ON
   THE CIRCUIT, not chrome: it scales with the desk camera, and Settings ▸
   Appearance ▸ Editor font size must never touch it. A `--font-size-*` token
   here would put a screen-pixel size into world space and slide the label off
   the part it names. The tell is the fractional value: a sub-pixel font-size is
   always world units. (`.annotation-text` and `.wire-gauge-length` are exempt
   for the same reason and say so in place.) */
.board-row-label,
.board-col-label {
  fill: var(--color-board-label);
  font-family: var(--font-sans);
  font-size: 0.6px; /* SVG user units = pitch units */
  user-select: none;
}

/* Placement ghost: a translucent breadboard kit tracking the cursor. Its
   strips sit at their kit offsets inside it, so the outline frames the whole
   assembly rather than each piece. */
.board-ghost {
  position: absolute;
  opacity: 0.55;
  border-radius: 6px;
}

.board-ghost-strip {
  position: absolute;
}

/* A pasted DESIGN (Feature 240): boards, parts, and wiring ghosted as one
   rigid arrangement, so only this wrapper moves with the cursor. Its children
   are laid out once in the clip's own coordinates. */
.design-ghost {
  position: absolute;
  opacity: 0.6;
}

/* Zero-size SVG renders nothing (per spec), so the wire ghost is a token 1×1
   box that paints outside itself — the paths carry their own coordinates. */
.design-ghost-wires {
  position: absolute;
  left: 0;
  top: 0;
  overflow: visible;
  pointer-events: none;
}

/* Per STRIP, not per design: the ghost's wrapper has no size of its own (its
   children carry the geometry), so the "will this land?" outline goes on the
   boards themselves — the same green/red the kit ghost uses. */
.board-ghost-strip--legal {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.board-ghost-strip--illegal {
  outline: 2px solid var(--color-danger);
  outline-offset: 1px;
  background: color-mix(in srgb, var(--color-danger) 25%, transparent);
}

.board-ghost--legal {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.board-ghost--illegal {
  outline: 2px solid var(--color-danger);
  outline-offset: 2px;
}

.board-ghost--illegal::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--color-danger);
  opacity: 0.2;
  border-radius: 6px;
}

/* ── Parts (DIP chips, Feature 40) ───────────────────────────────────────── */
.part {
  position: absolute;
}

.part > svg {
  display: block;
}

.part--selected {
  /* Uses the "Selection border colour" setting (--color-selection), the same
     token the board-outline highlighter strokes with; falls back to accent. */
  outline: 2px solid var(--color-selection, var(--color-accent));
  outline-offset: 1px;
  border-radius: 4px;
}

.part--dragging {
  opacity: 0.85;
}

.part--illegal {
  outline: 2px solid var(--color-danger);
  outline-offset: 1px;
  border-radius: 4px;
}

/* …and filled, as the refused board outline is: the wash is what reads at a
   glance beside orange selection rims. ::before because a part's ::after is
   its underpowered dot. A span part washes its own frame instead (below) —
   its element box is padded for the body at any angle. */
.part--illegal::before {
  content: "";
  position: absolute;
  inset: -1px; /* out to the outline's inner edge (outline-offset) */
  border-radius: 5px;
  background: color-mix(in srgb, var(--color-danger) 25%, transparent);
  pointer-events: none;
}

.part-chip-body {
  fill: var(--color-chip-body);
  stroke: var(--color-chip-notch);
  stroke-width: 0.06;
}

.part-chip-notch {
  fill: var(--color-chip-notch);
}

.part-chip-leg {
  fill: var(--color-chip-leg);
}

.part-chip-dot {
  fill: var(--color-chip-label);
  opacity: 0.85;
}

.part-chip-label {
  fill: var(--color-chip-label);
  font-family: var(--font-mono);
  font-size: 0.8px; /* SVG user units = pitch units */
  letter-spacing: 0.06px;
  user-select: none;
}

/* A CUSTOM chip (the chip designer): its own plastic, a folded corner and the
   `</>` glyph (chip-view.js buildCustomMarks) — distinct by shape as well as
   colour. The same marks head it in the tray and on the designer's package. */
.part-chip--custom .part-chip-body,
.chip-custom-body {
  fill: var(--color-chip-custom-body);
}

.part-chip-fold {
  fill: var(--color-chip-custom-fold);
}

.part-chip-glyph {
  fill: none;
  stroke: var(--color-chip-custom-glyph);
  stroke-width: 0.07;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The chip debugger on the board: an armed chip's breakpoint badge, and a
   pulsing outline while it is paused in its own code. */
.part-chip-breakpoint {
  display: none;
  fill: var(--color-debug-break);
  stroke: var(--color-chip-label);
  stroke-width: 0.05;
}

.part-chip--armed .part-chip-breakpoint {
  display: inline;
}

.part-chip--paused {
  outline: 2px solid var(--color-debug-pause);
  outline-offset: 2px;
  border-radius: 4px;
  animation: part-chip-pause-pulse 1.1s ease-in-out infinite;
}

@keyframes part-chip-pause-pulse {
  50% {
    outline-color: transparent;
  }
}

@media (prefers-reduced-motion: reduce) {
  .part-chip--paused {
    animation: none;
  }
}

/* ── Discrete parts (Feature 60): switch, button, LED ────────────────────── */
.part-body {
  fill: var(--color-part-body);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-slide-slot {
  fill: var(--color-part-inset);
}

.part-slide-knob {
  fill: var(--color-part-accent);
}

.part-button-cap {
  fill: var(--color-part-cap);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
  pointer-events: auto; /* the press target — the view owns the gesture */
}

.part-discrete--pressed .part-button-cap {
  fill: var(--color-danger);
  transform: scale(0.88);
  transform-origin: center;
  transform-box: fill-box;
}

.part-toggle-cap {
  fill: var(--color-part-cap);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-toggle-cap--on {
  fill: var(--color-success);
  transform: scale(0.88);
  transform-origin: center;
  transform-box: fill-box;
}

/* DIP switch bank (sw-dip1/2/4/8): a chip-shaped body — taller, as the real
   part is — with n slide actuators, each in its own .part-dip-switch group
   (see discrete-view.js's buildDipSwitchBank). The ON band marks the row-f
   edge as "closed", and carries the word, so which way is ON never rests on
   telling the green nub from the red one. */
.part-dip-on-bar {
  fill: var(--color-part-inset);
}

.part-dip-on-label {
  fill: var(--color-chip-label);
  font-family: var(--font-mono);
  font-size: 0.44px; /* SVG user units = pitch units */
  letter-spacing: 0.03px;
  user-select: none;
}

.part-dip-slot {
  fill: var(--color-part-inset);
}

.part-dip-nub {
  fill: var(--color-part-cap);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
  cursor: pointer;
}

.part-dip-nub--on {
  fill: var(--color-success);
}

.part-led-leg {
  fill: var(--color-chip-leg);
}

/* Unlit LED: a dim body tint until the simulator drives it (Feature 90).
   LED colors share the wire color tokens (house rule). */
.part-led-dome {
  fill: var(--wire-color, var(--color-wire-red));
  fill-opacity: 0.45;
  stroke: var(--color-part-inset);
  stroke-width: 0.07;
}

.part-led-dome--red {
  --wire-color: var(--color-wire-red);
}

.part-led-dome--green {
  --wire-color: var(--color-wire-green);
}

.part-led-dome--yellow {
  --wire-color: var(--color-wire-yellow);
}

.part-led-dome--blue {
  --wire-color: var(--color-wire-blue);
}

.part-led-dome--white {
  --wire-color: var(--color-wire-white);
}

.part-led-flat {
  fill: var(--color-part-inset);
}

/* Resistor: metal leads, beige body, dark color bands. Purely cosmetic —
   electrically it's a weak coupler (pull-up/pull-down/series R). */
.part-resistor-lead {
  fill: var(--color-chip-leg);
}

/* A span part's lead is a stroked line (any angle, rail↔column). */
.part-span-lead {
  stroke: var(--color-chip-leg);
  stroke-width: 0.14;
  stroke-linecap: round;
}

/* A part spanning a rail and a far row covers a tall, mostly-empty box, so only
   its lead takes the pointer — every hole under the box stays clickable. The
   widened invisible hit stroke mirrors .wire-hit. Every two-lead (rotatable)
   part draws as a span, so the view's own --span class says which. */
.part-discrete--span {
  pointer-events: none;
}

.part-span-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 1.6;
  stroke-linecap: round;
  pointer-events: stroke;
}

/* The LED's leads are a thin span right where its wires attach, so the lead
   line makes a poor grab handle. Make the dome (centred on the pin-to-pin
   midpoint) a second pointer target so a click on it selects and drags the
   whole LED, leaving the lead line (and the wires there) free to click.
   Placed LEDs only: the placement ghost lives in the pointer-inert overlay
   and must stay that way. */
.part-discrete--led .part-led-dome {
  pointer-events: fill;
  cursor: grab;
}

/* A bent lead that touches no hole — what a part is left with when the strip
   under it is moved or deleted away. The part keeps its exact position and
   span (the bend is geometry, not an address), so only the lead changes: it
   goes dashed and takes the warning tint to show the connection is gone. */
.part-discrete--floating .part-span-lead {
  stroke: var(--color-warning);
  stroke-dasharray: 0.36 0.28;
  stroke-linecap: butt;
}

/* Smoke rises past the span's viewBox, so let it escape (same trick the wire
   layer uses for its zero-size anchor). */
.part-discrete-svg--rotated {
  overflow: visible;
}

/* A span part's selection frame (discrete-view.js spanFrame): .part--selected's
   outline, drawn INSIDE the part so it hugs the leads and body and turns with
   them — the element's own box is padded for the body at any angle, and
   outlined it framed a small part in a great deal of empty board. */
.part-span-frame {
  fill: none;
  stroke-width: 0.2;
  visibility: hidden;
  pointer-events: none;
}

.part--selected .part-span-frame {
  visibility: visible;
  stroke: var(--color-selection, var(--color-accent));
}

.part--illegal .part-span-frame {
  visibility: visible;
  stroke: var(--color-danger);
  fill: color-mix(in srgb, var(--color-danger) 25%, transparent);
}

.part-discrete--span.part--selected,
.part-discrete--span.part--illegal {
  outline: none;
}

.part-discrete--span.part--illegal::before {
  content: none;
}

/* ── Part fault symbols (built by part-symbols.js, shared by LEDs + chips) ── */
/* Both stay hidden until a view adds its status class — usually while the sim
   runs, though a chip's "unprogrammed ROM" class (Feature 190 follow-up)
   applies at design time too; the reveal rules live with each part family
   (burnt LEDs below, chips in the Feature 90 section, oscillator cans
   further down). */
.part-burn,
.part-warn {
  display: none;
}

.part-discrete--burnt .part-burn {
  display: block;
}

/* A burnt LED never glows, whatever the levels say. */
.part-discrete--burnt .part-led-dome {
  fill: var(--color-part-inset);
}

/* A powered, behavioral discrete (currently: an oscillator can)
   gets the same fault symbols a chip does; see DiscreteView.setStatus. */
.part-discrete--unpowered .part-warn {
  display: block;
}

.part-discrete--reversed .part-burn,
.part-discrete--damaged .part-burn,
.part-discrete--overloaded .part-burn {
  display: block;
}

/* Spice Lite's brown smoke (an oscillator can driven past twice its
   budget): the chip's burn, in the brown smoke the chip's is drawn in. */
.part-discrete--overloaded .part-burn-smoke {
  fill: var(--color-part-smoke-brown);
}

.part-discrete--underpowered::after {
  content: "";
  position: absolute;
  top: -3px;
  right: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--color-base);
  background: var(--color-warning);
}

/* ── Segment displays (seg8cc / bar8): a dark package block standing over a row
   of holes, its LED segments lit individually. Segments share the wire-color
   tokens (house rule), inherited from --wire-color set on the SVG root. ────── */
.part-display-body {
  fill: var(--color-chip-body);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

/* A DIP-bodied display's pin-1 mark (the bar array's), a can's dot. */
.part-display-pin1 {
  fill: var(--color-part-inset);
}

/* Unlit segment: a dim tint of the display's colour until the sim drives it. */
.part-seg {
  fill: var(--wire-color, var(--color-wire-red));
  fill-opacity: 0.14;
  stroke: var(--color-part-inset);
  stroke-width: 0.04;
}

/* Lit segment (Feature 90 rule): full colour + a glow halo, exactly as the lit
   LED dome. Spice Lite sets --led-level by its current (1 at its datasheet's;
   the halo is world px, like the dome's). */
.part-seg--lit {
  fill-opacity: calc(0.3 + 0.7 * min(1, var(--led-level, 1)));
  filter: drop-shadow(0 0 calc(0.4px * var(--led-level, 1)) var(--wire-color))
    drop-shadow(0 0 calc(0.7px * var(--led-level, 1)) var(--wire-color));
}

/* Over-driven segment — conducting with no series resistor: dark, no glow,
   the same fate a burnt LED dome gets (wins over --lit). */
.part-seg--burnt {
  fill: var(--color-part-inset);
  fill-opacity: 1;
  filter: none;
}

/* The block covers a wide, mostly-empty box, so only its body takes the
   pointer — every hole under the row stays clickable. A transparent hit rect
   over the block reports the drag/select (same trick as .part-span-hit). A
   DIP switch bank instead layers several .part-display-hit rects (one per
   switch position, one for the body) so each actuator addresses itself.
   DiscreteView sets --body-hit on every drawing that HAS one: a hand-kept list
   of refs fell behind (the 7-segments, the regulators, the relay). */
.part-discrete--body-hit {
  pointer-events: none;
}

.part-display-hit {
  fill: transparent;
  pointer-events: fill;
}

.part-warn-tri {
  fill: var(--color-warning);
  stroke: var(--color-base);
  stroke-width: 0.08;
  stroke-linejoin: round;
}

/* The exclamation mark, punched out of the triangle in the desk colour. */
.part-warn-mark {
  fill: var(--color-base);
  stroke: var(--color-base);
  stroke-width: 0.12;
  stroke-linecap: round;
}

.part-burn-x {
  stroke: var(--color-danger);
  stroke-width: 0.26;
  stroke-linecap: round;
}

.part-burn-smoke {
  fill: var(--color-part-smoke);
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

/* The smoke rises only where a reveal rule SHOWS the burn (a burnt LED or
   diode, a chip or can reversed, killed or overloaded). Declared on every
   puff, the infinite animation ran whether its overlay was displayed or not:
   four puffs on every LED and chip, restyled and laid out again every frame,
   burnt or not, running or stopped — 416 on the busy fixture, most of each
   frame's style and layout work (make profile). */
@media (prefers-reduced-motion: no-preference) {
  .part-discrete--burnt .part-burn-smoke,
  .part-discrete--reversed .part-burn-smoke,
  .part-discrete--damaged .part-burn-smoke,
  .part-discrete--overloaded .part-burn-smoke,
  .part-chip--reversed .part-burn-smoke,
  .part-chip--damaged .part-burn-smoke,
  .part-chip--overloaded .part-burn-smoke {
    animation: part-burn-smoke 2.4s ease-out infinite;
  }
}

/* Nearly opaque at the base and slow to climb: sooty smoke reads by density,
   and the colour alone can't carry it on a dark desk. Holding full opacity to
   45% before the fade keeps a continuous plume rather than a blink. */
@keyframes part-burn-smoke {
  0% {
    opacity: 0;
    transform: translateY(0) scale(0.4);
  }
  18% {
    opacity: 0.95;
  }
  45% {
    opacity: 0.85;
  }
  100% {
    opacity: 0;
    transform: translateY(-3.2px) scale(1.7);
  }
}

@media (prefers-reduced-motion: reduce) {
  .part-burn-smoke {
    opacity: 0.7;
  }
}

/* ── Oscillator can: a plain metal body — no notch, unlike a chip — with
   legs only at its 4 corners; free to seat anywhere and spin in place. ──── */
.part-can-body {
  fill: var(--color-chip-leg);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-can-rim {
  fill: none;
  stroke: var(--color-part-inset);
  stroke-width: 0.05;
  opacity: 0.6;
}

.part-can-dot {
  fill: var(--color-part-inset);
}

.part-can-badge {
  fill: var(--color-part-inset);
  font-family: var(--font-mono);
  font-size: 0.75px;
  font-weight: 600;
  user-select: none;
}

.part-discrete--unpowered .part-can-badge,
.part-discrete--reversed .part-can-badge,
.part-discrete--damaged .part-can-badge,
.part-discrete--overloaded .part-can-badge {
  opacity: 0.25;
}

.part-resistor-body {
  fill: var(--color-part-resistor);
  stroke: var(--color-part-inset);
  stroke-width: 0.07;
}

.part-resistor-band {
  fill: var(--color-part-inset);
}

/* The colour code, one class per band colour (model/resistor-bands.js). */
.part-resistor-band--black {
  fill: var(--color-band-black);
}
.part-resistor-band--brown {
  fill: var(--color-band-brown);
}
.part-resistor-band--red {
  fill: var(--color-band-red);
}
.part-resistor-band--orange {
  fill: var(--color-band-orange);
}
.part-resistor-band--yellow {
  fill: var(--color-band-yellow);
}
.part-resistor-band--green {
  fill: var(--color-band-green);
}
.part-resistor-band--blue {
  fill: var(--color-band-blue);
}
.part-resistor-band--violet {
  fill: var(--color-band-violet);
}
.part-resistor-band--grey {
  fill: var(--color-band-grey);
}
.part-resistor-band--white {
  fill: var(--color-band-white);
}
.part-resistor-band--gold {
  fill: var(--color-band-gold);
}
.part-resistor-band--silver {
  fill: var(--color-band-silver);
}

/* Capacitors: a ceramic disc face-on, and an electrolytic's can from above
   with its negative stripe. The value is printed on each, upright. */
.part-cap-ceramic {
  fill: var(--color-part-ceramic);
  stroke: var(--color-part-inset);
  stroke-width: 0.07;
}

.part-cap-can {
  fill: var(--color-part-cap-can);
  stroke: var(--color-part-inset);
  stroke-width: 0.07;
}

.part-cap-stripe {
  fill: var(--color-part-cap-stripe);
}

.part-cap-minus {
  stroke: var(--color-part-cap-can);
  stroke-width: 0.1;
  stroke-linecap: round;
}

.part-cap-label {
  font-family: var(--font-mono);
  font-size: 0.5px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

.part-cap-label--cap-ceramic {
  fill: var(--color-part-ceramic-text);
}

.part-cap-label--cap-electrolytic {
  fill: var(--color-part-cap-text);
}

/* Bussed resistor array (rnet9): a beige SIP body over one row of legs, printed
   with its value and a dot at the common-bus end. */
.part-rnet-body {
  fill: var(--color-part-resistor);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-rnet-dot {
  fill: var(--color-part-inset);
}

.part-rnet-label {
  fill: var(--color-part-inset);
  font-family: var(--font-mono);
  font-size: 0.8px; /* SVG user units = pitch units */
  letter-spacing: 0.04px;
  user-select: none;
}

/* Trimmer potentiometer (pot): a blue block centred over its three pins,
   printed with its value, and the brass screw whose slot says where the wiper
   is. */
.part-pot-body {
  fill: var(--color-part-trimpot);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-pot-label {
  fill: var(--color-part-trimpot-text);
  font-family: var(--font-mono);
  font-size: 0.6px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

.part-pot-screw {
  fill: var(--color-part-screw);
  stroke: var(--color-part-screw-slot);
  stroke-width: 0.05;
}

.part-pot-slot {
  stroke: var(--color-part-screw-slot);
  stroke-width: 0.11;
  stroke-linecap: round;
}

.part-pot-pointer {
  fill: var(--color-part-trimpot-text);
}

/* ── The discretes (catalog/discretes.js) ──────────────────────────── */

/* A diode: a short body with the band on its cathode end — black epoxy and a
   silver band for a rectifier, amber glass and a black band for a Zener. */
.part-diode-body {
  fill: var(--color-part-diode);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-diode-band {
  fill: var(--color-part-diode-band);
}

.part-diode-body--zener {
  fill: var(--color-part-zener);
  fill-opacity: 0.85;
}

.part-diode-band--zener {
  fill: var(--color-part-zener-band);
}

/* A burnt diode goes dark like a burnt LED's dome. */
.part-discrete--burnt .part-diode-body {
  fill: var(--color-part-inset);
}

/* An inductor, in its two looks, from above. A TOROID standing on edge: the
   top of its ring, copper wound round the dark ferrite — each turn a copper
   stroke over the darker edge of the next, the core showing between them. A
   CAN: a drum in a black sleeve, the bevel of its rim a little lighter than
   its top, its value printed there. */
.part-inductor-core {
  fill: var(--color-part-inductor-core);
  stroke: var(--color-part-inset);
  stroke-width: 0.05;
}

.part-inductor-turn-edge {
  stroke: var(--color-part-inductor-wire);
  stroke-width: 0.12;
  stroke-linecap: round;
}

.part-inductor-turn {
  stroke: var(--color-part-inductor-copper);
  stroke-width: 0.07;
  stroke-linecap: round;
}

.part-inductor-drum {
  fill: var(--color-part-inductor-rim);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-inductor-drum-top {
  fill: var(--color-part-inductor-drum);
}

.part-inductor-label {
  fill: var(--color-part-inductor-text);
  font-family: var(--font-mono);
  font-size: 0.45px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

/* A part number printed BESIDE a two-lead part (a diode's body has no room
   for one), upright whatever angle its leads run at. */
.part-span-label {
  fill: var(--color-part-inset);
  font-family: var(--font-mono);
  font-size: 0.42px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

/* A transistor, from above, in its package: a TO-92 (the D of its moulding)
   or a TO-220 (its moulding, and the metal tab behind it). Either face is
   printed with its part number and its pin letters, and carries a lamp that
   lights while it conducts, ringed amber while a MOSFET holds its last
   state. */
.part-to92-body,
.part-to220-body {
  fill: var(--color-part-transistor);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-to220-tab {
  fill: var(--color-part-transistor-tab);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-to220-tab-line {
  stroke: var(--color-part-inset);
  stroke-width: 0.04;
  opacity: 0.6;
}

.part-transistor-label,
.part-transistor-pin {
  fill: var(--color-part-transistor-text);
  font-family: var(--font-mono);
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

.part-transistor-label {
  font-size: 0.36px; /* SVG user units = pitch units */
}

/* A TO-220's face has more room than a TO-92's. */
.part-transistor-label--to220 {
  font-size: 0.42px; /* SVG user units = pitch units */
}

.part-transistor-pin {
  font-size: 0.3px; /* SVG user units = pitch units */
  opacity: 0.8;
}

.part-transistor-lamp {
  fill: var(--color-part-inset);
  stroke: none;
}

.part-discrete--on .part-transistor-lamp {
  fill: var(--color-part-on);
  filter: drop-shadow(0 0 0.25px var(--color-part-on));
}

.part-discrete--held .part-transistor-lamp {
  stroke: var(--color-part-held);
  stroke-width: 0.07;
}

/* A relay (the blue cube of a Songle SRD) over its five legs: its rating and
   its legs' names on the face, and the transistor's lamp, lit while it is
   pulled in. */
.part-relay-body {
  fill: var(--color-part-relay);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-relay-label,
.part-relay-pin {
  fill: var(--color-part-relay-text);
  font-family: var(--font-mono);
  font-weight: 600;
  user-select: none;
  pointer-events: none;
}

.part-relay-label {
  font-size: 0.6px; /* SVG user units = pitch units */
}

.part-relay-label--small {
  font-size: 0.36px; /* SVG user units = pitch units */
  opacity: 0.85;
}

.part-relay-pin {
  font-size: 0.26px; /* SVG user units = pitch units */
  opacity: 0.85;
}

/* A speaker (a 12 mm PCB one) over its two legs: the black can, its cone
   and dust cap, and a ring round the cone lit while it plays (Spice Lite). */
.part-speaker-body {
  fill: var(--color-part-speaker);
  stroke: var(--color-part-speaker-rim);
  stroke-width: 0.08;
}

.part-speaker-ring {
  fill: none;
  stroke: var(--color-part-inset);
  stroke-width: 0.12;
}

.part-discrete--playing .part-speaker-ring {
  stroke: var(--color-part-on);
  filter: drop-shadow(0 0 0.25px var(--color-part-on));
}

.part-speaker-cone {
  fill: var(--color-part-speaker-cone);
  stroke: var(--color-part-inset);
  stroke-width: 0.05;
}

.part-speaker-cap {
  fill: var(--color-part-speaker);
  stroke: var(--color-part-speaker-rim);
  stroke-width: 0.05;
}

.part-speaker-label {
  fill: var(--color-part-speaker-text);
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 0.36px; /* SVG user units = pitch units */
  user-select: none;
  pointer-events: none;
}

/* The pitch it plays, on the cone below the dust cap — empty while silent. */
.part-speaker-hz {
  fill: var(--color-part-on);
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 0.42px; /* SVG user units = pitch units */
  user-select: none;
  pointer-events: none;
}

/* Direct-use sensor modules. Their electrical behavior lives in the catalog;
   these shapes deliberately match the workbench's compact native SVG style. */
.part-sensor-board {
  fill: #176b50;
  stroke: #0a3528;
  stroke-width: 0.12;
}

.part-sensor-board--contact,
.part-sensor-board--resistive {
  fill: #216b75;
}

.part-sensor-pin {
  stroke: #c7a44b;
  stroke-width: 0.18;
  stroke-linecap: round;
}

.part-sensor-label,
.part-sensor-value {
  fill: #f4f7ef;
  font-family: var(--font-mono);
  font-size: 0.44px;
  font-weight: 700;
  text-anchor: middle;
  pointer-events: none;
}

.part-sensor-value {
  fill: #bee7d8;
  font-size: 0.33px;
  font-weight: 500;
}

.part-sensor-disc {
  fill: #d7ba73;
  stroke: #4f3a1f;
  stroke-width: 0.12;
}

.part-sensor-indicator {
  fill: #26342f;
  stroke: #9db9ad;
  stroke-width: 0.08;
}

.part-sensor-indicator--on {
  fill: #ff493f;
  filter: drop-shadow(0 0 0.16px #ff493f);
}

.part-sensor-joystick-base {
  fill: #20272a;
  stroke: #aeb8bb;
  stroke-width: 0.13;
}

.part-sensor-joystick-knob {
  fill: #3a4246;
  stroke: #111719;
  stroke-width: 0.12;
}

.part-sensor-joystick-knob--pressed {
  fill: #252b2e;
  stroke: #7cd3b1;
}

.part-sensor-key {
  fill: #303642;
  stroke: #a8b0bd;
  stroke-width: 0.08;
}

.part-sensor-key--pressed {
  fill: #f0b429;
  stroke: #ffe6a3;
}

.part-sensor-key-label {
  fill: #fff;
  font-family: var(--font-mono);
  font-size: 0.46px;
  font-weight: 700;
  text-anchor: middle;
  pointer-events: none;
}

.part-sensor-encoder {
  fill: #b8bdc5;
  stroke: #535a64;
  stroke-width: 0.12;
}

.part-sensor-encoder-mark {
  stroke: #353a42;
  stroke-width: 0.18;
  stroke-linecap: round;
}

.part-sensor-hit {
  fill: transparent;
  pointer-events: all;
}

/* ── PSU brick (desk-level) ──────────────────────────────────────────────── */
.part-psu-body {
  fill: var(--color-psu-body);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-psu-badge {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 1.2px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
}

.part-psu-terminal--plus {
  fill: var(--color-wire-red);
}

.part-psu-terminal--minus {
  fill: var(--color-wire-black);
}

.part-psu-terminal-glyph {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.7px;
  user-select: none;
  pointer-events: none;
}

/* ── Electronic load brick (load-view.js) ───────────────────────────────── */
/* The PSU's body in its own colour, its setting on the badge, and under
   Spice Lite what it draws: volts and amps, then watts — amber while it
   cannot draw what it is set to (UNREG). Its terminals are the PSU's. */
.part-load-body {
  fill: var(--color-load-body);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-load-badge {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 1px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
}

.part-load-readout,
.part-load-watts {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  opacity: 0.85;
  user-select: none;
}

.part-load-readout {
  font-size: 0.9px; /* SVG user units = pitch units */
}

.part-load-watts {
  font-size: 0.75px; /* SVG user units = pitch units */
}

.part-load--unreg .part-load-readout,
.part-load--unreg .part-load-watts {
  fill: var(--color-psu-limited);
  opacity: 1;
}

/* ── Clock source brick (Feature 100) ────────────────────────────────────── */
.part-clock-body {
  fill: var(--color-psu-body);
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

.part-clock-badge {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.99px; /* SVG user units = pitch units */
  font-weight: 600;
  user-select: none;
}

.part-clock-wave {
  fill: none;
  stroke: var(--color-psu-badge);
  stroke-width: 0.14;
  stroke-linejoin: round;
  opacity: 0.65;
}

/* Pulse lamp: dim until the output goes HIGH, then bright with a glow. */
.part-clock-lamp {
  fill: var(--color-part-inset);
  stroke: var(--color-part-inset);
  stroke-width: 0.05;
}

.part-clock--high .part-clock-lamp {
  fill: var(--color-sim-high);
  filter: drop-shadow(0 0 0.5px var(--color-sim-high))
    drop-shadow(0 0 0.9px var(--color-sim-high));
}

.part-clock-terminal {
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-clock-terminal--out {
  fill: var(--color-wire-orange);
}

.part-clock-terminal--vcc {
  fill: var(--color-wire-red);
}

.part-clock-terminal--gnd {
  fill: var(--color-wire-black);
}

.part-clock-terminal-glyph {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.7px;
  user-select: none;
  pointer-events: none;
}

/* A free-running clock's own pause button (top right). Stopped there is no
   clock to pause and a press drags the brick, so it exists only on a RUNNING
   desk — the class the editing lock sets. It shows what a click would DO, as
   the transport's Pause does: ⏸ while the clock runs, ▶ while it is held. */
.part-clock-pause {
  display: none;
}

.desk-viewport--running .part-clock-pause {
  display: inline;
}

.part-clock-pause-disc {
  fill: var(--color-part-inset);
  stroke: var(--color-psu-badge);
  stroke-width: 0.06;
  stroke-opacity: 0.35;
}

.part-clock-pause:hover .part-clock-pause-disc {
  stroke-opacity: 1;
}

/* The brick's own ink, not a status colour: the brick is dark in BOTH themes
   (its tokens never flip), so a token that darkens for the light theme would
   sink into it. */
.part-clock-pause-glyph {
  fill: var(--color-psu-badge);
  pointer-events: none;
}

.part-clock-pause-glyph--resume,
.part-clock--paused .part-clock-pause-glyph--pause {
  display: none;
}

.part-clock--paused .part-clock-pause-glyph--resume {
  display: inline;
}

/* A held clock's wave fades out — it is not oscillating. The lamp still shows
   the level it is holding. */
.part-clock--paused .part-clock-wave {
  opacity: 0.2;
}

/* ── Character-LCD module (HD44780) ──────────────────────────────────────── */
/* The bare PCB the module is bonded to — green, so it reads as a board and not
   as part of the black display above it. */
.part-lcd-body {
  fill: var(--color-lcd-pcb);
  stroke: var(--color-part-inset);
  stroke-width: 0.1;
}

/* The display module itself: the dark metal frame the glass is recessed into,
   sitting proud of the green board around it. */
.part-lcd-window {
  fill: var(--color-lcd-bezel);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

/* Corner mounting holes — plated, like the header's. */
.part-lcd-mount {
  fill: var(--color-board-hole);
  stroke: var(--color-chip-leg);
  stroke-width: 0.09;
}

/* The backlit field. --lcd-screen is set per module by buildCharacterDisplay,
   so a colour change is one custom property and no redrawn geometry. Under
   Spice Lite LcdView.setPanel sets --lcd-backlight (0–1, the backlight LED's
   brightness by its current): unlit, the glass is the bezel's dark with a
   little of its tint left — a reflective panel with no light behind it. */
.part-lcd-panel {
  fill: color-mix(
    in srgb,
    var(--lcd-screen, var(--color-lcd-screen-green))
      calc(35% + 65% * var(--lcd-backlight, 1)),
    var(--color-lcd-bezel)
  );
  stroke: var(--color-part-inset);
  stroke-width: 0.08;
}

/* The 16-way header, printed where the module plugs into the board. */
.part-lcd-header {
  fill: color-mix(in srgb, var(--color-lcd-bezel) 60%, #ffffff);
  stroke: var(--color-part-inset);
  stroke-width: 0.06;
}

.part-lcd-hole {
  fill: var(--color-chip-leg);
}

/* The live characters are drawn onto a canvas sitting over the panel. Nearest-
   neighbour scaling keeps the 5×8 dots crisp; it's pointer-inert so drags land
   on the body div. */
.part-lcd-screen {
  position: absolute;
  image-rendering: pixelated;
  pointer-events: none;
  /* Spice Lite: how dark VDD − V0 drives the characters (LcdView.setPanel). */
  opacity: var(--lcd-contrast, 1);
}

.part-lcd-pin1-label {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.55px;
  user-select: none;
  pointer-events: none;
}

.part-lcd-size {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.9px;
  opacity: 0.7;
  user-select: none;
  pointer-events: none;
}

/* Power faults dim the screen (mirrors the chip fault feedback). The status
   classes come from DiscreteView.setStatus, like every other seated part's. */
.part-discrete--unpowered .part-lcd-panel,
.part-discrete--underpowered .part-lcd-panel {
  opacity: 0.4;
}

.part-discrete--reversed .part-lcd-panel,
.part-discrete--damaged .part-lcd-panel,
.part-discrete--overloaded .part-lcd-panel {
  fill: var(--color-part-smoke);
}

/* Part placement ghost (palette pick → seat/drop). */
.part-ghost {
  position: absolute;
  opacity: 0.6;
  border-radius: 4px;
}

.part-ghost--legal {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.part-ghost--illegal {
  outline: 2px solid var(--color-danger);
  outline-offset: 1px;
}

.part-ghost--illegal::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--color-danger);
  opacity: 0.25;
  border-radius: 4px;
}

/* ── Auto-route debug overlay (Feature 360) ──────────────────────────────────
   Option-clicking Auto-route leaves the router's own view of the desk behind:
   the legal region, the bridges off it, the hole-free lanes it discounts, and
   every obstacle AS INFLATED — the gap between an obstacle's drawn edge and its
   inflated one is exactly one wire's clearance, which is the thing hardest to
   picture from the numbers. Pointer-inert like everything else in the overlay. */
.route-debug {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
  pointer-events: none;
}

.route-debug-region {
  fill: none;
  stroke: var(--color-accent);
  stroke-width: 1.5;
  stroke-dasharray: 6 4;
  opacity: 0.7;
}

.route-debug-bridge {
  fill: var(--color-accent);
  fill-opacity: 0.07;
  stroke: var(--color-accent);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

.route-debug-lane {
  stroke: var(--color-accent);
  stroke-width: 0.5;
  opacity: 0.25;
}

.route-debug-obstacle {
  fill: var(--color-danger);
  fill-opacity: 0.14;
  stroke: var(--color-danger);
  stroke-width: 1;
  opacity: 0.8;
}

/* ── Wires (Feature 50): one SVG in .layer-wires ─────────────────────────── */
.desk-viewport--wiring {
  cursor: crosshair;
}

.wire-svg {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

/* ── Signal flags (Feature 370) ──────────────────────────────────────────── */
/* A pointed pentagon whose apex plugs into one hole. The polygon is its own
   hit target — a sanctioned per-item event exception, like .wire-hit below:
   ten arbitrary rotated pentagons is exactly where idiomatic SVG beats
   hand-rolled distance math. Colour comes in as --signal-color, per element,
   because a custom property cannot be set through el()'s style bag. */
.signal-svg {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

/* How much of a flag's colour its body shows — ONE number, read by the desk
   flags, the paste ghost's flags, the rail's waiting chip, the key disc
   (which derives its own from it), and the Output/Input tags and their
   waiting chips. */
.signal-svg,
.design-ghost-signals,
.signal-flag-chip-art,
.tag-svg,
.integration-tag-chip {
  --signal-flag-alpha: 0.4375;
}

.signal-flag {
  fill: var(--signal-color);
  /* The body reads as a tinted field with a solid border of the SAME colour,
     matched to its button's dot. Colours repeat once the seven are used, so
     the KEY printed inside (.signal-flag-key) is what names the flag. */
  fill-opacity: var(--signal-flag-alpha);
  stroke: var(--signal-color);
  stroke-width: 1; /* world px (0.1 pitch) — the polygon is drawn in px */
  stroke-linejoin: round;
  cursor: grab;
}

/* The paste ghost's flags: no per-signal colour yet (the paste re-issues one
   on arrival), so they take the accent the rest of the ghost uses. */
.design-ghost-signals {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.design-ghost-signals .signal-flag {
  --signal-color: var(--color-accent);
}

.signal-flag--selected {
  stroke: var(--color-selection, var(--color-accent));
  stroke-width: 2;
}

/* The digit key that presses this signal, upright in the middle of the flag's
   body on the same coloured disc its rail button shows it on — but
   translucent, so the board shows through. Pointer-inert, so the polygon stays
   the one hit target. */
.signal-flag-key {
  pointer-events: none;
  user-select: none;
}

/* Laid over the body, the disc must read as exactly TWICE the body's opacity:
   a over a composites to a + d(1 - a), which is 2a when d = a / (1 - a). So it
   is derived from the body's own alpha rather than typed (0.4375 → ~0.78, for
   a combined 0.875). */
.signal-flag-key-disc {
  fill: var(--signal-color);
  fill-opacity: calc(var(--signal-flag-alpha) / (1 - var(--signal-flag-alpha)));
}

/* The rail dot's ink (a fixed dark, since it sits on the signal's own colour),
   in the type an Output/Input tag prints its pin number in — the flag is the
   tag's glyph, so the two must read the same. */
.signal-flag-key-digit {
  fill: var(--color-wire-black);
  font-size: 9.75px; /* world px (0.975 pitch) — printed on the flag */
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-anchor: middle;
  dominant-baseline: central;
}

/* Dragged somewhere the drop would achieve nothing — a hole another lead
   holds, or bare desk with no planted flag to pull out. The flag keeps
   following the cursor and turns red, the way every other desk drag reports an
   illegal target; it does NOT jump home, because only a drop reverts. */
.signal-flag--illegal {
  fill: var(--color-danger);
  stroke: var(--color-danger);
}

/* ── Output / Input tags (the Arduino serial integration) ───────────────── */
/* A signal flag's glyph exactly (model/integration.js's TAG_W is FLAG_W —
   over one pitch, so tags in adjacent holes overlap), sharing the flag's
   colour rules: a tinted body with a solid border of the element's colour. The trigger tag is dashed — it LISTENS for the edge; the pin tags
   carry the value. */
.tag-svg {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.integration-tag {
  fill: var(--signal-color);
  fill-opacity: var(--signal-flag-alpha);
  stroke: var(--signal-color);
  stroke-width: 1; /* world px (0.1 pitch) — the polygon is drawn in px */
  stroke-linejoin: round;
  cursor: grab;
}

/* An Input DRIVES its net, so its tags read more solid than an Output's,
   which only sample. */
.integration-tag--input {
  fill-opacity: calc(2 * var(--signal-flag-alpha));
}

.integration-tag--trigger {
  stroke-dasharray: 3 2; /* world px, like the stroke */
}

.integration-tag--selected {
  stroke: var(--color-selection, var(--color-accent));
  stroke-width: 2;
  stroke-dasharray: none;
}

.integration-tag--illegal {
  fill: var(--color-danger);
  stroke: var(--color-danger);
}

/* The pin number (or the trigger's edge arrow), upright on a plate in the
   element's colour — the flag's key disc at tag size, so the holes under the
   translucent body never cross the number. Pointer-inert, so the polygon stays
   the one hit target. */
.integration-tag-label {
  font-size: 9.75px; /* world px (0.975 pitch) — printed on the tag */
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  user-select: none;
}

/* Laid over the body, the plate reads as TWICE the body's opacity — the key
   disc's rule (.signal-flag-key-disc): a / (1 - a) over a composites to 2a. */
.integration-tag-plate {
  fill: var(--signal-color);
  fill-opacity: calc(var(--signal-flag-alpha) / (1 - var(--signal-flag-alpha)));
}

/* FIXED dark ink, as the flag's digit has: the board is cream in BOTH themes,
   so the app's text colour (near-white in dark mode) all but vanished on it. */
.integration-tag-number {
  fill: var(--color-wire-black);
  text-anchor: middle;
  dominant-baseline: central;
}

/* Widened invisible hit stroke — the one sanctioned per-item event target. */
.wire-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 10;
  pointer-events: stroke;
}

/* While the wire tool is armed, wires must not swallow hole clicks. */
.desk-viewport--wiring .wire-hit {
  pointer-events: none;
}

/* A wire whose end is being dragged: the pointer is captured, so its own hit
   stroke must not intercept the gesture. */
.wire--dragging .wire-hit {
  pointer-events: none;
}

.wire-outline {
  fill: none;
  stroke: var(--color-wire-outline);
  stroke-width: 4;
  stroke-linecap: round;
  pointer-events: none;
}

.wire-core {
  fill: none;
  stroke: var(--wire-color);
  stroke-width: 2.4;
  stroke-linecap: round;
  pointer-events: none;
}

.wire-cap {
  fill: var(--wire-color);
  stroke: var(--color-wire-outline);
  stroke-width: 0.8;
  pointer-events: none;
}

.wire--selected .wire-outline {
  /* The "Selection border colour" setting (--color-selection), the same as the
     board outline + selected annotations; falls back to the accent. */
  stroke: var(--color-selection, var(--color-accent));
  stroke-width: 5;
}

/* A ROUTED wire (its "Layout Method" property): the run is a straight
   polyline, and pressing anywhere along it bends it — hence the grab cursor,
   which is the only hint the plain line gives before it is touched. */
.wire--routed .wire-hit {
  cursor: grab;
}

/* One waypoint's knob. Hidden until the wire is hovered or picked, so a routed
   wire at rest draws as the bare line it is; --active is the one under a
   captured pointer mid-drag (CSS :hover can't follow it there), and --merging
   says releasing now drops this point into the one it is sitting on. Same
   knob-over-invisible-target idiom as a bus end handle, minus the target: a
   waypoint is grabbed by geometry (WireTools), not by a DOM hit circle. */
.wire-point {
  fill: var(--color-surface-0);
  stroke: var(--wire-color);
  stroke-width: 1.2;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
}

.wire:hover .wire-point,
.wire--selected .wire-point,
.wire-point--active {
  opacity: 1;
}

.wire-point--merging {
  fill: var(--color-danger);
  stroke: var(--color-danger);
}

/* While the circuit RUNS the wiring is frozen — no drag can start on a wire,
   no bend can be inserted, no wire menu opens — so the affordances that say
   otherwise go away with it: a routed wire's waypoint knobs stay hidden
   however it is hovered or picked (a knob is a grab target, and there is
   nothing to grab), and its body drops the grab cursor for the plain arrow
   .desk-viewport--running already sets. Both selectors deliberately outrank
   the rules above by DEPTH rather than by document order, so neither can be
   undone by a later addition to this block. Picking a wire still HIGHLIGHTS
   it — that is the one thing left to do with one, and WireLayer draws the
   picked wire last so its whole run reads over everything it passes under. */
.desk-viewport--running .wire .wire-point {
  opacity: 0;
}

.desk-viewport--running .wire--routed .wire-hit {
  cursor: default;
}

/* Rubber-band preview while a wire is pending. */
.wire-preview {
  opacity: 0.75;
}

.wire-preview--illegal .wire-core {
  stroke: var(--color-danger);
}

/* ── Buses (Feature 130): bundle bands over the member wires ─────────────── */
.desk-viewport--bus {
  cursor: crosshair;
}

/* The ribbon's flat, cut-edge body: a darker outline under a colored fill
   (the .wire-outline/.wire-core idiom, just wider), sized to the bit count
   via --ribbon-width. A butt cap reads as a flat cable end, not a wire tip.
   Rendered AFTER every member lead (see WireLayer#buildBandCover) and fully
   opaque, so it paints OVER the base of each lead right where they meet it —
   the leads read as entering a solid body, not just touching its edge. */
.bus-band-outline {
  fill: none;
  stroke: var(--color-wire-outline);
  stroke-width: calc(var(--ribbon-width, 12) + 3);
  stroke-linecap: butt;
  pointer-events: none;
}

.bus-band-fill {
  fill: none;
  stroke: var(--wire-color);
  stroke-width: var(--ribbon-width, 12);
  stroke-linecap: butt;
  pointer-events: none;
}

/* Widened invisible hit stroke — clicking the band (in the gaps between the
   member wires) selects the whole bus. */
.bus-band-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 16;
  pointer-events: stroke;
  cursor: grab;
}

/* While either wiring tool is armed, bands must not swallow hole clicks. */
.desk-viewport--wiring .bus-band-hit,
.desk-viewport--bus .bus-band-hit,
.desk-viewport--wire-dragging .bus-band-hit,
.desk-viewport--wiring .bus-end-handle-hit,
.desk-viewport--bus .bus-end-handle-hit,
.desk-viewport--wire-dragging .bus-end-handle-hit {
  pointer-events: none;
}

/* One end's grab target: an always-live, invisible widened hit circle (finds
   the cursor before the band's own hit stroke does — smaller, more specific
   target wins) plus a small knob that fades in on hover, or stays shown for
   the whole gesture once THIS end is the one being dragged (--active — a
   captured pointer can wander off the tiny circle mid-drag without CSS
   :hover following it there). */
.bus-end-handle-hit {
  fill: transparent;
  pointer-events: all;
  cursor: grab;
}

.bus-end-handle-knob {
  fill: var(--color-surface-0);
  stroke: var(--wire-color);
  stroke-width: 1.6;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.1s ease;
}

.bus-end-handle:hover .bus-end-handle-knob,
.bus-end-handle--active .bus-end-handle-knob {
  opacity: 1;
}

.bus-end-handle--active .bus-end-handle-hit {
  cursor: grabbing;
}

.bus-band-label {
  fill: var(--color-text);
  font-size: 5px; /* world px — counter-scaling isn't needed at these sizes */
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: middle;
  paint-order: stroke;
  stroke: var(--color-surface-0);
  stroke-width: 1.4;
  pointer-events: none;
  user-select: none;
}

.bus-band--selected .bus-band-fill {
  stroke: var(--color-selection, var(--color-accent));
}

.bus-band--illegal .bus-band-fill {
  stroke: var(--color-danger);
}

.bus-band--illegal .bus-end-handle--active .bus-end-handle-knob {
  stroke: var(--color-danger);
}

.bus-band--preview {
  opacity: 0.85;
}

/* ── Hover addressing (ring + tooltip in the overlay layer) ─────────────── */
.hole-ring {
  position: absolute;
  /* Diameter in world px, box-sizing: border-box — so this IS the outer
     circle. It must stay 2 × RING_RADIUS × PX_PER_UNIT: that constant is what
     desk-controller / wire-tools / bus-tools / probe-inspector each offset the
     ring by to centre it on the hole, and a mismatch shows up as a ring that
     sits a pixel off the target rather than as anything obviously broken. */
  width: 11px; /* 1.1 pitch in world px */
  height: 11px;
  border: 1px solid var(--color-info);
  border-radius: 50%;
}

.hole-ring--illegal {
  border-color: var(--color-danger);
}

/* Option/Alt held — pin-targeting mode: the one hole under the pointer that
   holds an independently movable lead (a wire end, a two-terminal part's leg).
   RED rather than the selection colour, so a candidate reads differently from
   what is already picked; a little heavier, since it is the only thing lit. */
.hole-ring--pin {
  border: 2px solid var(--color-danger);
}

/* The lead an Option/Alt-click picked — in the selection colour, like every
   other selected thing. */
.hole-ring--selected {
  border: 2px solid var(--color-selection, var(--color-accent));
}

/* ── Connectivity inspector (Feature 70) ─────────────────────────────────── */
/* One SVG in the overlay layer lighting up every point of one net. Token
   1×1 box + overflow visible (a 0-size <svg> renders nothing). */
.net-highlight {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
  pointer-events: none;
}

.net-highlight-dot {
  fill: var(--color-net-glow);
  fill-opacity: 0.6;
}

.net-highlight-pin {
  fill: none;
  stroke: var(--color-net-glow);
  stroke-width: 1.4;
  stroke-opacity: 0.9;
}

.net-highlight-wire {
  fill: none;
  stroke: var(--color-net-glow);
  stroke-width: 5;
  stroke-opacity: 0.5;
  stroke-linecap: round;
}

/* Pinned: brighter/thicker so a locked net reads as locked. */
.net-highlight--pinned .net-highlight-dot {
  fill-opacity: 0.85;
}

.net-highlight--pinned .net-highlight-pin {
  stroke-width: 1.8;
}

.net-highlight--pinned .net-highlight-wire {
  stroke-opacity: 0.7;
}

/* Net summary readout — a screen-fixed chip, bottom-left. */
.net-status {
  position: absolute;
  left: var(--space-4);
  bottom: var(--space-4);
  max-width: 60%;
  padding: var(--space-1) var(--space-3);
  background: var(--color-surface-0);
  border: 1px solid var(--color-net-glow);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  user-select: none;
}

.net-status--pinned {
  border-width: 2px;
  font-weight: 600;
}

/* A named net (Feature 120) leads its readout with the user name in accent. */
.net-status--named {
  color: var(--color-accent);
}

/* ── Two-probe multimeter ───────────────────────────────────────────────── */
.multimeter-panel {
  position: absolute;
  z-index: 12;
  top: var(--space-3);
  right: var(--space-3);
  width: min(24rem, calc(100% - 2 * var(--space-3)));
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-0);
  box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 35%);
  color: var(--color-text);
}

.multimeter-panel[hidden] {
  display: none;
}

.multimeter-close {
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  color: var(--color-muted);
  font-size: calc(var(--font-size) + 7px);
  cursor: pointer;
}

.multimeter-drag-handle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 1.75rem;
  margin: calc(-1 * var(--space-2)) calc(-1 * var(--space-2)) var(--space-1);
  padding: var(--space-1) var(--space-2);
  color: var(--color-subtext);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.multimeter-panel--dragging .multimeter-drag-handle {
  cursor: grabbing;
}

.multimeter-title {
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.multimeter-modes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.multimeter-mode,
.multimeter-lead {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: inherit;
  cursor: pointer;
}

.multimeter-mode {
  min-width: 3rem;
  padding: var(--space-1) var(--space-2);
  font-family: var(--font-mono);
}

.multimeter-mode--active {
  border-color: var(--color-accent);
  background: var(--color-surface-0);
}

.multimeter-display {
  display: block;
  margin: var(--space-3) 0 var(--space-1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: #c9d5b1;
  color: #172116;
  font: 700 calc(var(--font-size) + 16px)/1.25 var(--font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.multimeter-hint {
  min-height: 1.3em;
  color: var(--color-muted);
  font-size: var(--font-size-xs);
}

.multimeter-leads {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.multimeter-lead {
  overflow: hidden;
  padding: var(--space-2);
  font-family: var(--font-mono);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.multimeter-lead--red {
  border-left: 0.35rem solid var(--color-wire-red);
}

.multimeter-lead--black {
  border-left: 0.35rem solid var(--color-wire-black);
}

.multimeter-lead--active {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.multimeter-probe {
  position: absolute;
  z-index: 14;
  width: 17px;
  height: 17px;
  border: 3px solid;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: rgb(255 255 255 / 22%);
  box-shadow:
    0 0 0 1px rgb(255 255 255 / 95%),
    0 0 0 3px rgb(0 0 0 / 35%),
    0 0 8px 3px currentColor;
  pointer-events: none;
}

.multimeter-probe::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 4px;
  height: 4px;
  border: 1px solid rgb(255 255 255 / 90%);
  border-radius: 50%;
  background: currentColor;
  transform: translate(-50%, -50%);
}

.multimeter-probe--red {
  color: var(--color-wire-red);
  border-color: var(--color-wire-red);
}

.multimeter-probe--black {
  color: var(--color-wire-black);
  border-color: var(--color-wire-black);
}

/* ── Two-channel oscilloscope ───────────────────────────────────────────── */
.desk-viewport--scoping {
  cursor: crosshair;
}

.oscilloscope-panel {
  position: absolute;
  z-index: 11;
  right: var(--space-3);
  bottom: calc(var(--space-3) + 3rem);
  width: min(40rem, calc(100% - 2 * var(--space-3)));
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-0);
  box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 35%);
  color: var(--color-text);
}

.oscilloscope-panel[hidden] {
  display: none;
}

.oscilloscope-close {
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  color: var(--color-muted);
  font-size: calc(var(--font-size) + 7px);
  cursor: pointer;
}

.oscilloscope-drag-handle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 1.75rem;
  margin: calc(-1 * var(--space-2)) calc(-1 * var(--space-2)) var(--space-1);
  padding: var(--space-1) var(--space-2);
  color: var(--color-subtext);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.oscilloscope-panel--dragging .oscilloscope-drag-handle {
  cursor: grabbing;
}

.oscilloscope-title {
  font-size: var(--font-size-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.oscilloscope-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-xs);
}

.oscilloscope-select,
.oscilloscope-control,
.oscilloscope-trigger-level,
.oscilloscope-offset,
.oscilloscope-probe-button {
  min-height: 1.8rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: inherit;
  font: inherit;
}

.oscilloscope-control,
.oscilloscope-probe-button {
  cursor: pointer;
}

.oscilloscope-control--active {
  border-color: var(--color-accent);
  background: var(--color-surface-0);
}

.oscilloscope-trigger-level {
  width: 4.5rem;
  padding: 0 var(--space-1);
}

.oscilloscope-trigger-position {
  width: 5rem;
  accent-color: var(--color-accent);
}

.oscilloscope-scale-button {
  min-width: 1.8rem;
  padding: 0 var(--space-1);
  font-weight: 700;
}

.oscilloscope-channels,
.oscilloscope-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-top: var(--space-1);
}

.oscilloscope-channel {
  display: flex;
  flex: 1 1 14rem;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  padding-left: var(--space-1);
  border-left: 0.25rem solid;
  font-size: var(--font-size-xs);
}

.oscilloscope-channel--ch1 {
  border-color: #ffd84a;
}

.oscilloscope-channel--ch2 {
  border-color: #52d7ff;
}

.oscilloscope-offset {
  width: 3.75rem;
  padding: 0 var(--space-1);
}

.oscilloscope-actions .oscilloscope-control {
  padding: var(--space-1) var(--space-2);
}

.oscilloscope-screen {
  display: block;
  width: 100%;
  margin-top: var(--space-2);
  border: 1px solid #496149;
  border-radius: var(--radius-sm);
  background: #071107;
  aspect-ratio: 5 / 2;
}

.oscilloscope-grid {
  stroke: #254125;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.oscilloscope-axis-tick {
  stroke: rgb(210 230 210 / 70%);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.oscilloscope-axis-label {
  font:
    8px ui-monospace,
    SFMono-Regular,
    Consolas,
    monospace;
  paint-order: stroke;
  stroke: #071107;
  stroke-width: 3px;
  stroke-linejoin: round;
  pointer-events: none;
  user-select: none;
}

.oscilloscope-axis-label--time {
  fill: #c4d3c4;
}

.oscilloscope-axis-label--ch1 {
  fill: #ffd84a;
}

.oscilloscope-axis-label--ch2 {
  fill: #52d7ff;
}

.oscilloscope-zero-marker--ch1 {
  fill: #ffd84a;
}

.oscilloscope-zero-marker--ch2 {
  fill: #52d7ff;
}

.oscilloscope-trigger-marker {
  stroke: rgb(255 255 255 / 45%);
  stroke-width: 1;
  stroke-dasharray: 4 4;
  vector-effect: non-scaling-stroke;
}

.oscilloscope-trace {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.oscilloscope-trace--ch1 {
  stroke: #ffd84a;
}

.oscilloscope-trace--ch2 {
  stroke: #52d7ff;
}

.oscilloscope-readout {
  display: block;
  min-height: 1.4em;
  margin-top: var(--space-1);
  color: var(--color-subtext);
  font: var(--font-size-xs) var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.oscilloscope-measurements {
  min-height: 2.9em;
  margin-top: var(--space-1);
  color: var(--color-subtext);
  font: var(--font-size-xs) var(--font-mono);
  font-variant-numeric: tabular-nums;
  line-height: 1.45;
  white-space: pre-line;
}

.oscilloscope-cursor-controls {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-1) var(--space-2);
  margin-top: var(--space-2);
  font-size: var(--font-size-xs);
}

.oscilloscope-cursor-controls[hidden] {
  display: none;
}

.oscilloscope-cursor-controls label {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr);
  align-items: center;
  gap: var(--space-1);
}

.oscilloscope-cursor-readout {
  grid-column: 1 / -1;
  color: var(--color-subtext);
  font-family: var(--font-mono);
}

.oscilloscope-cursor {
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke;
}

.oscilloscope-cursor--a {
  stroke: #ff9f43;
}

.oscilloscope-cursor--b {
  stroke: #bc8cff;
}

.oscilloscope-probes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.oscilloscope-probe-button {
  overflow: hidden;
  padding: var(--space-1) var(--space-2);
  font-family: var(--font-mono);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.oscilloscope-probe-button--ch1 {
  border-left: 0.35rem solid #ffd84a;
}

.oscilloscope-probe-button--ch2 {
  border-left: 0.35rem solid #52d7ff;
}

.oscilloscope-probe-button--ground {
  border-left: 0.35rem solid #e8e8e8;
}

.oscilloscope-probe-button--active {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.oscilloscope-probe {
  position: absolute;
  z-index: 14;
  width: 15px;
  height: 15px;
  border: 3px solid;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  box-shadow:
    0 0 0 1px rgb(255 255 255 / 95%),
    0 0 0 3px rgb(0 0 0 / 35%),
    0 0 7px 2px currentColor;
  pointer-events: none;
}

.oscilloscope-probe--ch1 {
  color: #ffd84a;
}

.oscilloscope-probe--ch2 {
  color: #52d7ff;
}

.oscilloscope-probe--ground {
  color: #e8e8e8;
}

@media (max-width: 600px) {
  .oscilloscope-panel {
    bottom: var(--space-2);
  }

  .oscilloscope-probes {
    grid-template-columns: 1fr;
  }

  .oscilloscope-cursor-controls {
    grid-template-columns: 1fr;
  }

  .oscilloscope-cursor-readout {
    grid-column: 1;
  }
}

/* ── Annotations: labels & notes (Feature 120) ───────────────────────────── */
/* One absolutely-positioned box per annotation in .layer-annotations (above
   wires, below the overlay). x/y are the box's top-left in world px. */
.annotation {
  position: absolute;
  --annotation-color: var(--color-text);
  max-width: 320px;
  color: var(--annotation-color);
  cursor: grab;
  user-select: none;
}

/* Deliberately OFF the type scale, here and on the editor below: an annotation
   is DOCUMENT CONTENT living in the zoom-scaled world layer, not chrome. On the
   scale, the same saved desk would read differently on two machines at the same
   zoom — the note would no longer sit where its author placed it. It grows with
   the desk camera, as the chip labels it is written beside do. */
.annotation-text {
  padding: 2px 6px;
  font-family: var(--font-sans);
  font-size: 12px;
  line-height: 1.35;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* A label is a bare one-line caption; a note is a bordered multi-line box. */
.annotation--label .annotation-text {
  font-weight: 600;
  white-space: nowrap;
}

/* A label cannot wrap, so its width is intrinsic — the shared max-width would
   only shrink the BOX (its selection outline and its grab area) under the text
   it draws. It stays on .annotation--note, where it is what makes one wrap. */
.annotation--label {
  max-width: none;
}

.annotation--note {
  min-width: 80px;
  background: color-mix(in srgb, var(--color-surface-0) 88%, transparent);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
}

.annotation-text--empty {
  color: var(--color-muted);
  font-style: italic;
  font-weight: 400;
}

/* An anchored label rides its part; a faint left tick marks the tether. */
.annotation--anchored {
  border-left: 2px solid var(--annotation-color);
  padding-left: 2px;
}

.annotation--selected {
  outline: 2px solid var(--color-selection, var(--color-accent));
  outline-offset: 1px;
  border-radius: 2px;
}

.annotation--editing {
  cursor: text;
}

/* The inline editor swaps in for the text on double-click. */
.annotation-editor {
  min-width: 120px;
  padding: 2px 6px;
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: 12px;
  resize: none;
}

textarea.annotation-editor {
  min-height: 3.2em;
  min-width: 160px;
}

/* The placement ghost tracks the cursor before the drop. */
.annotation-ghost {
  opacity: 0.7;
  pointer-events: none;
}

.annotation-ghost--anchored {
  outline: 1px dashed var(--color-accent);
  outline-offset: 2px;
}

.desk-tooltip {
  position: absolute;
  /* Counter-scaled by DeskController (--inv-zoom = 1/zoom) so the label is
     the same apparent size at every zoom; offset applies in screen px. */
  transform: scale(var(--inv-zoom, 1)) translate(8px, calc(-100% - 8px));
  transform-origin: 0 0;
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  white-space: nowrap;
}

/* ── Zoom control cluster (bottom-right overlay) ─────────────────────────── */
/* ── The desk padlock (components/desk-lock.js) ──────────────────────────────
   Top-right of the desk, and TRANSPARENT: it sits directly on the desk with no
   card of its own, so a mostly-empty corner stays empty-looking. It lines up
   with the tab strip's top edge across the desk from it. */
.desk-lock {
  position: absolute;
  top: var(--space-2);
  right: var(--space-3);
  display: grid;
  place-items: center;
  width: var(--desk-lock-size);
  height: var(--desk-lock-size);
  padding: 0;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  /* Muted at rest so an unlocked desk reads as having no control on it at all;
     the whole point of the thing is to be out of the way until wanted. */
  color: var(--color-muted);
  cursor: pointer;
}

.desk-lock:hover {
  color: var(--color-text);
}

/* Shut: the accent, so the state is legible from across the desk — and the icon
   itself changes shape, since colour alone is not a state (see desk-lock.js). */
.desk-lock--locked {
  color: var(--color-accent);
}

.desk-lock:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.desk-zoom {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  /* Height stated through the shared token rather than left to content, so the
     signal rail above it can subtract a number that is actually true — and
     the width likewise, for the activity lamps beside it. */
  height: var(--desk-zoom-height);
  width: var(--desk-zoom-width);
  box-sizing: border-box;
  padding: var(--space-1);
  background: var(--color-surface-0);
  border: 1px solid var(--color-crust);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.desk-zoom-btn,
.desk-zoom-readout {
  display: grid;
  place-items: center;
  height: calc(var(--font-size) + 11px);
  border: none;
  background: transparent;
  border-radius: var(--radius-sm);
  color: var(--color-subtext);
}

.desk-zoom-btn {
  width: calc(var(--font-size) + 11px);
  font-size: var(--font-size-lg);
  line-height: 1;
}

.desk-zoom-readout {
  flex: 1 1 auto;
  min-width: 48px;
  padding: 0 var(--space-1);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
}

.desk-zoom-btn:enabled:hover,
.desk-zoom-readout:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.desk-zoom-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ── Popup host, dialogs & menus (popup-manager.js, ported pattern) ─────── */
/* A bare top-layer host: the native <dialog> centres its .popup card via the
   UA default margins; the menu variant instead pins a .popup-menu at screen
   coordinates. */
.popup-dialog {
  padding: var(--space-4);
  border: none;
  background: transparent;
  max-width: 100vw;
  max-height: 100vh;
  overflow: visible;
}

.popup-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.popup {
  min-width: 320px;
  max-width: 460px;
  max-height: 80vh;
  overflow: auto;
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}

/* Every popup header is a ROW: the title takes the slack, so whatever trails it
   — dialog()'s close (×), the pinout window's datasheet/example buttons — sits
   hard right with nothing per-dialog to say so. Four dialogs each carried their
   own copy of these three lines, and the one that didn't (the keyboard-shortcuts
   card) showed its × pressed up against the title. */
.popup-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) 0;
}

.popup-title {
  flex: 1;
  min-width: 0;
  font-size: var(--font-size-xl);
  font-weight: 600;
}

.popup-body {
  padding: var(--space-3) var(--space-4);
}

.popup-message {
  font-size: var(--font-size);
  color: var(--color-text);
}

.popup-note {
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  word-break: break-all;
}

.popup-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: 0 var(--space-4) var(--space-4);
}

/* Prompt dialog (Feature 120): a labelled text field + reserved-name picks. */
.popup-label {
  display: block;
  margin-bottom: var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.popup-input {
  width: 100%;
  height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size);
}

.popup-input:focus {
  outline: none;
  border-color: var(--color-accent);
}

.popup-quickpicks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.popup-quickpick {
  height: auto;
  padding: var(--space-1) var(--space-3);
}

/* Buttons (shared with later dialogs). */
.btn {
  height: var(--control-height);
  padding: 0 var(--space-4);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-text);
  font-size: var(--font-size);
}

.btn:hover {
  background: var(--color-surface-0);
}

.btn--primary {
  border-color: var(--color-accent);
}

.btn--secondary {
  background: transparent;
}

.btn--danger {
  border-color: var(--color-danger);
  color: var(--color-danger);
}

/* ── Keyboard shortcuts dialog ─────────────────────────────────────────────── */
.shortcuts-popup {
  max-width: 520px;
}

.shortcuts-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-height: 60vh;
  overflow-y: auto;
}

.shortcuts-group-title {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-subtext);
}

.shortcuts-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.shortcuts-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-1) 0;
}

.shortcuts-desc {
  font-size: var(--font-size);
  color: var(--color-text);
}

.shortcuts-keys {
  flex: none;
  padding: 2px var(--space-2);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-subtext);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  white-space: nowrap;
}

/* ── About dialog (styled after Rest Hippo) ───────────────────────────────── */
.about-dialog {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-6) var(--space-6) var(--space-6);
  text-align: center;
  min-width: 340px;
  user-select: none;
}

.about-logo {
  width: 120px;
  height: 120px;
  border-radius: 22px;
  margin-bottom: var(--space-2);
}

.about-name-row {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.about-name {
  margin: 0;
  font-size: var(--font-size-display);
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: -0.02em;
}

/* The (i) beside a label (components/info-button.js). Sized off the type scale
   with a floor, like the ramp itself: it sits next to words, so it has to grow
   when they do — but it is already small at the default and must not shrink
   into an untargetable dot at the bottom of the range. */
.info-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: max(12px, calc(var(--font-size) - 1px));
  height: max(12px, calc(var(--font-size) - 1px));
  padding: 0;
  color: var(--color-accent);
  border: 1px solid var(--color-accent);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  transition:
    background 0.15s ease,
    color 0.15s ease;
}

.info-btn:hover,
.info-btn[aria-expanded="true"] {
  background: var(--color-accent);
  color: var(--color-crust);
}

/* The (i) as a MARK (info-button.js buildInfoMark): it explains on hover and
   does nothing on a click, so it does not point like a button. */
.info-btn--mark {
  cursor: help;
}

/* …recoloured red (a parts-tray group of limited parts), and nothing else. */
.info-btn--danger {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.info-btn--danger:hover {
  background: var(--color-danger);
  color: var(--color-crust);
}

/* …recoloured amber: ADVISORY (a value field's nearest-value hint) — worth
   knowing, never a fault, so never red. */
.info-btn--advisory {
  color: var(--color-warning);
  border-color: var(--color-warning);
}

.info-btn--advisory:hover,
.info-btn--advisory[aria-expanded="true"] {
  background: var(--color-warning);
  color: var(--color-crust);
}

.info-btn[hidden] {
  display: none;
}

/* Version/build popover — floats under the name so it doesn't reflow the card. */
.about-build {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 260px;
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  white-space: nowrap;
  text-align: left;
}

.about-build[hidden] {
  display: none;
}

.about-build-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-5);
}

.about-build-label {
  color: var(--color-subtext);
}

.about-build-value {
  color: var(--color-text);
  font-weight: 500;
}

.about-subtitle {
  margin: 0;
  font-size: var(--font-size);
  color: var(--color-text);
}

.about-desc {
  margin: 0;
  font-size: var(--font-size-xs);
  color: var(--color-subtext);
}

/* The footer's two lines — the credit with the copyright, the support address
   with the sponsor link — each two items with a dot between them, wrapping
   between items rather than inside one. Gaps rather than spaces in the catalog
   strings, so a translation cannot lose the separator between a label and the
   address (and a language that ends its label with a full-width colon does not
   get a stray Latin space after it). */
.about-credit,
.about-support {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-1) var(--space-2);
  margin: var(--space-1) 0 0;
  font-size: var(--font-size-xs);
}

.about-credit {
  color: var(--color-muted);
}

.about-support {
  /* The card sets `user-select: none`; the address is the one thing on it a
     user needs to be able to select and copy. */
  user-select: text;
}

.about-dot {
  color: var(--color-muted);
}

.about-support-item {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
}

.about-support-label {
  color: var(--color-subtext);
}

/* The voluntary sponsor link: a button (main opens the page) drawn as the link
   beside it. */
.about-donate {
  padding: 0;
  font: inherit;
  color: var(--color-accent);
  background: none;
  border: none;
  cursor: pointer;
  user-select: none;
}

.about-donate:hover,
.about-donate:focus-visible {
  text-decoration: underline;
}

.about-donate-heart {
  margin-right: var(--space-1);
}

.about-support-link {
  color: var(--color-accent);
  text-decoration: none;
}

.about-support-link:hover,
.about-support-link:focus-visible {
  text-decoration: underline;
}

.about-close {
  margin-top: var(--space-4);
  padding: var(--space-2) var(--space-6);
  font-size: var(--font-size);
  font-weight: 600;
  color: var(--color-crust);
  background: var(--color-accent);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: opacity 0.15s ease;
}

.about-close:hover {
  opacity: 0.85;
}

/* ── Settings dialog (styled after Rest Hippo: master-detail card) ────────── */
/* The card is sized in LINES, not pixels — 43 wide by 34 tall, which is 560
   wide at the shipped 13px. A fixed card would keep its width as the type
   grew, squeezing every label column until a label broke over three lines beside
   its own control; and this is the one dialog a user resizing the app is
   certainly looking at. Proportional rather than the additive law the CONTROLS
   follow, because a card holds paragraphs: its line LENGTH should scale with the
   type, where a button is one line plus constant padding.
   It grows but never SHRINKS (`max()`): the labels scale with the type while the
   nav rail and the controls' own padding do not, so a card that shrank in
   proportion at 11px squeezed the label column harder than 13px ever did — the
   same reason the type ramp itself carries a floor. `max-*` still clamps it to
   the window.
   The HEIGHT floor is what the tallest tab needs: Appearance, whose eight rows
   must all show without the panel scrolling. Its rows grow ADDITIVELY with the
   type (a control is one line plus padding) while 34 lines grows by 34 px a
   step, so the floor is what binds up to 14px and the lines take over above.
   The eighth row (3D enabled) fits only because every `.settings-row` was
   tightened from --space-3 to --space-2 padding at the same time. Measured in
   the running app, every shipped language at every size, as the panel's rows
   plus its padding against the 469px the 520 floor leaves it: the tallest
   needs 432px (Italian at 13px, whose font-size label wraps — 37px spare),
   and at 16 and 18 the 34 lines clear what is needed by 46px or more. A new
   Appearance row means measuring again — with the panel NOT scrolling, since
   a scrollbar narrows it and wraps more labels, overstating the need. */
.settings-popup {
  display: flex;
  flex-direction: column;
  width: max(560px, calc(var(--font-size) * 43));
  max-width: 90vw;
  height: max(520px, calc(var(--font-size) * 34));
  max-height: 85vh;
  overflow: hidden;
  padding: 0;
}

/* Header: title + a close (×) button, over a ruled mantle bar. The row layout is
   `.popup-header`'s own; this adds only the bar. */
.settings-popup .popup-header {
  padding: var(--space-3) var(--space-4);
  background: var(--color-mantle);
  border-bottom: 1px solid var(--color-crust);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  flex-shrink: 0;
}

.popup-close {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.popup-close:hover {
  background-color: var(--color-surface-1);
  color: var(--color-danger);
}

/* A dialog's own header buttons (dialog()'s `headerActions`) and its × as one
   tight group, top right: icons beside each other, not spread by the header's
   title gap. */
.popup-header-actions {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--space-1);
}

/* A header button is the ×'s box and look — the same affordance, a different
   glyph — and lifts to the accent rather than the ×'s danger red, since it
   opens something instead of dismissing. */
.popup-header-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.popup-header-btn:hover {
  background-color: var(--color-surface-1);
  color: var(--color-accent);
}

.popup-header-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* It sets a `display`, so it must opt back in to `hidden` (Settings shows its
   one only on the Serial I/O tab). */
.popup-header-btn[hidden] {
  display: none;
}

/* Master-detail body: left nav rail + right single-column panel. */
.settings-popup-body {
  display: flex;
  flex: 1 1 auto;
  padding: 0;
  overflow: hidden;
}

.settings-nav {
  flex: 0 0 auto;
  min-width: 148px;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-2);
  background: var(--color-mantle);
  border-right: 1px solid var(--color-crust);
  overflow-y: auto;
}

.settings-nav-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  font-size: var(--font-size);
  font-weight: 500;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.settings-nav-item:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.settings-nav-item--active {
  background: var(--color-accent);
  color: var(--color-crust);
  font-weight: 600;
}

.settings-panels {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-4) var(--space-5);
}

.settings-panel {
  display: flex;
  flex-direction: column;
}

/* Anything in this card that sets a `display` of its own must opt BACK IN to
   being hideable: a class selector outranks the UA sheet's
   `[hidden] { display: none }`, so `el.hidden = true` otherwise sets an
   attribute that changes nothing on screen. That is not theoretical — it is
   why the Data Sheets tab showed a **Clear** button with no folder to clear,
   and it caught About's **Restart to update** the same way, offered before
   anything had been downloaded. Grouped in one rule so the next control with
   a display and a hidden state is a line here, not another silent no-op. */
.settings-panel[hidden],
.settings-row[hidden],
.settings-note[hidden],
.settings-action[hidden] {
  display: none;
}

.settings-row {
  /* The containing block for this row's own note card (see .settings-note). */
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  /* --space-2, not -3: Appearance's eighth row (3D enabled) fits the card at
     every size only with the rows this close (see .settings-popup). */
  padding: var(--space-2) 0;
}

.settings-label {
  flex: 1;
  font-size: var(--font-size);
  color: var(--color-text);
  cursor: default;
}

/* A label plus the (i) that discloses its note. Takes the flex the bare label
   would have, so the control still sits hard right — and the label inside stops
   flexing, or the (i) would be pushed away from the words it belongs to. */
.settings-label-group {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1;
  min-width: 0;
}

.settings-label-group .settings-label {
  flex: 0 1 auto;
}

/* The note itself — a card FLOATING under its row, not a block pushing the rows
   below it down: a note that reflowed the panel would move the very control the
   reader is about to reach for. Positioned against `.settings-row`, which is why
   the note is a child of the row rather than its sibling. It is drawn like a
   popup card (surface, border, shadow) so it reads as something that opened
   rather than as part of the form; `z-index` clears the rows it covers. */
.settings-note {
  position: absolute;
  top: calc(100% - var(--space-2));
  left: 0;
  right: 0;
  z-index: 3;
}

/* The (i)'s card, wherever it opens: a Settings note, or a value field's
   nearest-value hint in a Properties card (.properties-hint) — ONE look, so
   the two read as the same control. Positioning is each one's own. */
.settings-note,
.info-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--color-surface-1);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.settings-color {
  width: 32px;
  height: 24px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
}

/* A dropdown for a setting with more choices than a segmented track can hold —
   today only Appearance ▸ Language (System plus one row per shipped catalog).
   Theme and Wire layout stay segmented: those are short either/or sets whose
   options should be readable without opening anything. */
.settings-select {
  flex-shrink: 0;
  height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size);
  cursor: pointer;
}

.settings-select:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* A stacked row (label on top of its control) — for wide controls like the
   datasheet-folder picker that would crowd the single-line flex layout. */
.settings-row--stack {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
}

/* A row whose control is a TEXT FIELD, laid out like every other row — label
   left, control right — rather than stacked. Stacked, a field costs two lines
   apiece, which is what ran the AI tab off the bottom of the card.
   The label takes its natural width but never less than a quarter of the row,
   so the fields on a tab line up in one column; a longer label in another
   language grows past that instead of wrapping under it, and `min-width: 0` is
   what lets the field give the space back. */
.settings-row--field .settings-label {
  flex: 0 0 auto;
  min-width: 25%;
}

.settings-row--field .settings-text-input {
  flex: 1 1 0;
  min-width: 0;
}

/* Buttons that act on the row above them, so they hug it and sit under the
   field rather than under the label. */
.settings-row--actions {
  justify-content: flex-end;
  gap: var(--space-2);
  padding-top: 0;
}

/* Datasheet-folder control: the current path (elided) above Clear/Browse. */
.settings-folder {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The path field spans the full panel width on its own line. */
.settings-folder-input {
  display: flex;
  align-items: center;
}

.settings-folder-path {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-text);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
}

.settings-folder-path--empty {
  color: var(--color-muted);
  font-style: italic;
}

/* The Clear + Browse buttons share a line under the input, aligned right. */
.settings-folder-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* The small action button of the Settings card, wherever a panel needs one:
   Browse… / Clear / Download… on Data Sheets, Save key / Clear key / Test
   connection on AI. */
.settings-action {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-weight: 500;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease,
    border-color 0.15s ease;
}

.settings-action:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* The DESTRUCTIVE ones: same shape, the danger colour. Both throw away
   something the user set up and cannot get back from here — the datasheet
   folder, and the stored API key. Both --danger rules must stay AFTER the
   base ones: same specificity, so source order is what makes red win. */
.settings-action--danger {
  color: var(--color-danger);
}

.settings-action--danger:hover {
  border-color: var(--color-danger);
  color: var(--color-danger);
  background: var(--color-surface-1);
}

/* ── Datasheet download progress (Settings ▸ Data Sheets ▸ Download…) ───────
   Its own popup rather than a row in the settings card: the run takes real
   time, can be cancelled, and can half-succeed, so it needs a counter, a bar,
   and room to name what did not arrive. */
.datasheet-dl-popup {
  width: 380px;
  max-width: 90vw;
  /* The header owns its own padding (shared with .properties-popup below). */
  padding: 0;
}

.datasheet-dl-status {
  margin: 0;
  font-size: var(--font-size);
  color: var(--color-text);
}

/* The track carries the border; the fill is the only moving part, so progress
   costs one style write per file rather than a re-layout. */
.datasheet-dl-bar {
  margin-top: var(--space-3);
  height: 6px;
  overflow: hidden;
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-lg);
}

.datasheet-dl-fill {
  width: 0;
  height: 100%;
  background: var(--color-accent);
  transition: width 0.15s ease;
}

/* The part in flight while running, the folder it all landed in when done. */
.datasheet-dl-detail {
  margin: var(--space-2) 0 0;
  min-height: 16px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* Named failures, scrollable — a summary that says "38 of 41" without saying
   which three is a folder listing the user has to do by hand. */
.datasheet-dl-failures {
  margin: var(--space-3) 0 0;
  padding: 0 0 0 var(--space-4);
  max-height: 120px;
  overflow-y: auto;
  font-size: var(--font-size-xs);
  line-height: 1.6;
  color: var(--color-danger);
}

.datasheet-dl-failures-head {
  list-style: none;
  margin-left: calc(-1 * var(--space-4));
  color: var(--color-muted);
}

.datasheet-dl-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-4);
}

/* A text/password field in a stacked row (AI ▸ base URL, model, API key).
   Matches .properties-text-input — same box, same focus ring — but scoped to
   the settings card, whose rows own their own spacing. */
.settings-text-input {
  width: 100%;
  box-sizing: border-box;
  height: var(--control-height);
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-0);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size);
}

.settings-text-input:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.settings-text-input:disabled {
  color: var(--color-muted);
  cursor: not-allowed;
}

.settings-hint {
  margin: var(--space-2) 0 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-muted);
}

/* ── Part Properties dialog (context menu → "Properties…") ─────────────────
   The one shared modal every part's properties open (part-properties-dialog.js).
   Styled like the Settings dialog's header/rows, but no nav rail — a part's
   property list is short and flat. */
.properties-popup {
  width: 360px;
  max-width: 90vw;
  /* The whole height the dialog has (the card sits inside the <dialog>'s own
     padding), not the generic 80vh: that cap scrolled an Output's card — the
     longest one here — by a few lines in an ordinary laptop window. When a
     card is taller still (an element of sixteen one-bit fields), the BODY
     scrolls under a header that stays put, not the whole card. */
  max-height: calc(100vh - 2 * var(--space-4));
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}

.properties-popup .popup-header {
  flex: none;
}

.properties-popup .popup-body {
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
}

/* Shared with the datasheet-download popup: the same small centred modal, so it
   wants the same titled mantle bar rather than a second copy of these rules. */
.properties-popup .popup-header,
.datasheet-dl-popup .popup-header {
  padding: var(--space-3) var(--space-4);
  background: var(--color-mantle);
  border-bottom: 1px solid var(--color-crust);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.properties-popup-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
}

.properties-row {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/* Name/Description (and any other text/textarea field): the label sits above
   a full-width control rather than beside a narrow one — a textarea doesn't
   fit the flex-shrink column the select/color rows share. */
.properties-row--stacked {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-2);
}

/* A row that means nothing under another row's current choice (a field's
   `disabledWhen` — an Output's "Trigger starts" on Auto): it keeps its place,
   so nothing below it moves, but reads as out of play and takes no pointer.
   Its controls are `disabled` as well, which is what stops the keyboard. */
.properties-row--disabled {
  opacity: 0.45;
  pointer-events: none;
}

.properties-label {
  flex: 1;
  font-size: var(--font-size);
  color: var(--color-text);
}

.properties-value {
  font-size: var(--font-size);
  color: var(--color-subtext);
}

/* A read-only Location: a file path, so it gets the stacked full-width row
   (STACKED_TYPES) and breaks anywhere rather than stretching the dialog. An
   object with no location yet shows an empty line, not a placeholder. */
.properties-value--path {
  min-height: 18px;
  overflow-wrap: anywhere;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

/* A read-only line of prose (a timer's Timing readout): stacked like a path,
   but wrapped at words in the body face. */
.properties-value--wrap {
  min-height: 18px;
  font-size: var(--font-size-sm);
  line-height: 1.45;
  white-space: pre-line;
}

/* A preset list and, for Custom…, its figures under it
   (components/preset-field.js): the list's row as any select's, then each
   figure a row of its own, set in under a rule so they read as the
   preset's parts rather than more of the card. */
.properties-preset {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.properties-preset-custom {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-left: var(--space-3);
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-overlay);
}

.properties-preset-custom[hidden] {
  display: none;
}

.properties-preset-custom .properties-combo {
  flex: 0 0 9em;
}

/* A component's value or part number (components/value-combobox.js): the
   Name box's look with a ▾ inside its right edge that drops the common
   values, and the field's own reason underneath when what was typed does not
   read. The list floats (position: fixed, placed against the box), so a card
   whose body scrolls never clips it. */
.properties-combo {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.properties-combo-field {
  position: relative;
  display: flex;
}

.properties-combo-input {
  flex: 1;
  min-width: 0;
  padding-right: calc(var(--control-height) + var(--space-1));
}

.properties-combo-toggle {
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  width: var(--control-height);
  padding: 0;
  border: none;
  border-left: 1px solid var(--color-overlay);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
}

/* A box with no list (a transistor's Custom figures) has no ▾ to clear. */
.properties-combo-input--bare {
  padding-right: var(--space-2);
}

.properties-combo-toggle[hidden] {
  display: none;
}

/* A box with a nearest-value hint: the box, then its (i) just to its right. */
.properties-combo-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.properties-combo-line > .properties-combo-field {
  flex: 1;
  min-width: 0;
}

/* The hint's card: an .info-card floating under its (i) — fixed, like the
   list, so a card whose body scrolls never clips it. */
.properties-hint {
  position: fixed;
  z-index: 2;
  max-width: 260px;
}

.properties-hint[hidden] {
  display: none;
}

.properties-hint-title {
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.properties-hint-values {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* Each neighbour a LINK: a click takes it. */
.properties-hint-value {
  padding: 0;
  border: none;
  background: none;
  font-family: var(--font-sans);
  font-size: var(--font-size);
  color: var(--color-accent);
  text-decoration: underline;
  cursor: pointer;
}

.properties-hint-value:hover {
  color: var(--color-text);
}

.properties-combo-toggle:hover {
  color: var(--color-text);
  background: var(--color-surface-1);
}

.properties-combo-list {
  position: fixed;
  z-index: 1;
  margin: 0;
  padding: var(--space-1) 0;
  list-style: none;
  overflow-y: auto;
  scrollbar-width: thin;
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
}

.properties-combo-list[hidden] {
  display: none;
}

.properties-combo-option {
  padding: var(--space-1) var(--space-3);
  font-family: var(--font-sans);
  font-size: var(--font-size);
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}

.properties-combo-option:hover,
.properties-combo-option--active {
  background: var(--color-surface-1);
}

.properties-combo-input[aria-invalid="true"] {
  border-color: var(--color-danger);
}

.properties-field-error {
  color: var(--color-danger);
  font-size: var(--font-size-xs);
  line-height: 1.4;
}

.properties-field-error[hidden] {
  display: none;
}

/* A slider (a potentiometer's Position): the track takes the row, and the
   value read out beside it in fixed-width figures, so the thumb's neighbour
   does not jitter as it moves from 9 % to 10 %. */
.properties-range {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.properties-range-input {
  flex: 1;
  min-width: 0;
  accent-color: var(--color-accent);
}

.properties-range-value,
.properties-range-end {
  min-width: 4.5ch;
  text-align: right;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

/* A slider whose ENDS are read out (a pot's two sides): each readout hugs its
   own end of the track, wide enough for the longest value ("4.75k") so the
   track does not shift as the values change length. */
.properties-range-end {
  min-width: 6ch;
}

.properties-range-end--end {
  text-align: left;
}

/* The shared segmented picker (components/segmented-picker.js): a DIALOG's
   form of the toolbar pill — one bordered track, borderless segments, the
   chosen one filled. Used by the Settings dialog (Theme, Wire layout, AI
   provider) and the Part Properties dialog's "segmented" field (a wire's
   Layout Method), so a short either/or choice reads the same in both. */
.segmented-picker {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex-shrink: 0;
  padding: var(--space-1);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
}

.segmented-option {
  height: var(--segment-height);
  padding: 0 var(--space-3);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

/* A track of NUMERALS rather than words (Appearance ▸ Editor font size). Two
   digits need less room around them than a word does, and this track carries
   six segments where the others carry two or three — at the shared padding it
   came out 246px wide and, since the track never shrinks, pushed its own label
   onto a second line. Tabular figures so the six segments are the same width. */
.segmented-picker--numeric .segmented-option {
  padding: 0 var(--space-2);
  font-variant-numeric: tabular-nums;
}

.segmented-option:hover {
  color: var(--color-text);
  background: var(--color-surface-1);
}

.segmented-option--active,
.segmented-option--active:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* An option the caller greys (a TO-220 on a TO-92's bent legs): there, but
   not offered — its title says why. */
.segmented-option:disabled,
.segmented-option:disabled:hover {
  background: transparent;
  color: var(--color-overlay);
  cursor: not-allowed;
}

/* The shared color picker (components/color-swatches.js): clickable circle
   swatches over the same --color-wire-<name> tokens the wire tool and the
   LED body use. Used by both the Part Properties dialog's "color" field and
   the Settings dialog's "Default LED color". The selected one gets a ring;
   hovering any swatch grows it slightly. */
.color-swatches {
  display: flex;
  gap: var(--space-2);
}

.color-swatch {
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--color-overlay);
  border-radius: 50%;
  background: var(--wire-color);
  cursor: pointer;
  box-shadow: 0 0 0 2px transparent;
  transition:
    box-shadow 0.15s ease,
    transform 0.15s ease;
}

.color-swatch:hover {
  transform: scale(1.1);
}

.color-swatch--selected {
  box-shadow: 0 0 0 2px var(--color-accent);
}

.color-swatch:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* A field's enum choice (PSU voltage, clock/oscillator rate, LCD size). */
.properties-select {
  flex-shrink: 0;
  height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size);
  cursor: pointer;
}

.properties-select:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* A control with its row's own command riding to its right (a field's
   `action` — the Connection picker's gear). The pair holds the control
   column together, so the label still takes the slack. */
.properties-control-group {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  gap: var(--space-1);
}

/* Borderless and square to the control beside it, like the Settings
   connection picker's + and bin (.integration-picker-btn). */
.properties-icon-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.properties-icon-action:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.properties-icon-action:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Name (text) / Description (textarea) — the two universal fields every
   Properties dialog opens with. */
.properties-text-input,
.properties-textarea {
  width: 100%;
  box-sizing: border-box;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size);
}

.properties-text-input {
  height: var(--control-height);
}

.properties-textarea {
  resize: vertical;
  min-height: 60px;
}

.properties-text-input:focus-visible,
.properties-textarea:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Divider between Name/Description and a part's own catalog properties. */
.properties-separator {
  width: 100%;
  height: 0;
  margin: 0;
  border: none;
  border-top: 1px solid var(--color-crust);
}

/* A field's command button (a memory chip's "Inspect memory…" / "Load
   image…") — full width, no separate label column. */
.properties-row--action {
  gap: 0;
}

.properties-action {
  flex: 1;
  height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-size: var(--font-size);
  cursor: pointer;
}

.properties-action:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* A drawing rather than a control (today: a wire's own dimensioned picture) —
   the full width of the card, so no label column and no gap to hold one. */
.properties-row--figure {
  gap: 0;
}

/* ── The warnings section (part-properties-dialog.js) ──────────────────────
   The faults a part is showing on the desk, spelled out under everything the
   dialog can edit. Its divider is this block's own `border-top` rather than a
   second `<hr>`, so hiding the section takes the rule with it. It comes and
   goes with the fault, which is why it needs its own `[hidden]` rule: this
   block sets a `display`, and a class selector outranks the UA sheet's
   `[hidden] { display: none }` — without the override, `hidden = true` would
   set an attribute that changes nothing at all. */
.properties-warnings {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-crust);
}

.properties-warnings[hidden] {
  display: none;
}

.properties-warnings-title {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-warning);
}

.properties-warning-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.properties-warning {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

/* The same sign the part draws, at text size — the triangle is stroked and
   the bang inside it filled, so it reads at 13 px the way the desk's does at
   its own scale. */
.properties-warning-icon {
  flex: none;
  margin-top: 1px;
  overflow: visible;
}

.properties-warning-icon path {
  fill: none;
  stroke: var(--color-warning);
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.properties-warning-icon circle {
  fill: var(--color-warning);
}

/* ── The wire gauge (components/wire-gauge.js) ─────────────────────────────
   A jumper drawn straight across the card in its own colour, stripped at both
   ends, with a dimension line under it. The viewBox fixes its proportions; the
   width is the card's and the height follows from the aspect ratio. */
.wire-gauge {
  display: block;
  width: 100%;
  height: auto;
}

/* Bare tinned lead — the same token the chips' legs use, because it is the same
   material: stripped, tinned copper. Round caps: a cut conductor has an end. */
.wire-gauge-lead {
  fill: none;
  stroke: var(--color-chip-leg);
  stroke-width: 3;
  stroke-linecap: round;
}

/* The insulation: the desk's own outline-under-core pair (.wire-outline /
   .wire-core), scaled up for a drawing rather than a desk, and BUTT-capped so
   each end reads as sleeve that was cut back. */
.wire-gauge-sleeve-outline {
  fill: none;
  stroke: var(--color-wire-outline);
  stroke-width: 11;
  stroke-linecap: butt;
}

.wire-gauge-sleeve {
  fill: none;
  stroke: var(--wire-color, var(--color-wire-red));
  stroke-width: 9;
  stroke-linecap: butt;
}

/* Dimension furniture: hairlines, so the wire stays the subject of the drawing
   and the measurement reads as an annotation over it. */
.wire-gauge-witness,
.wire-gauge-dim-line {
  fill: none;
  stroke: var(--color-subtext);
  stroke-width: 1;
  opacity: 0.6;
}

.wire-gauge-arrow {
  fill: var(--color-subtext);
  opacity: 0.6;
}

/* The measurement itself: mono, because it is a number that gets compared.
   OFF the type scale, like the stroke widths above it: this is a `<text>` in a
   300×62 viewBox drawn at `width: 100%`, so 12 is USER UNITS, not screen px —
   it scales with the card, and a token here would break the drawing's own
   proportions rather than resize a label. */
.wire-gauge-length {
  fill: var(--color-text);
  font-family: var(--font-mono);
  font-size: 12px;
  user-select: none;
}

/* Positioned popups (the context/dropdown menu and the popover): a
   full-viewport transparent host — it still catches the outside click that
   dismisses — holding a card PopupManager places at screen coordinates. */
.popup-dialog--menu,
.popup-dialog--popover {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
}

.popup-dialog--menu::backdrop,
.popup-dialog--popover::backdrop {
  background: transparent;
}

.popup-menu {
  position: fixed;
  min-width: 180px;
  display: flex;
  flex-direction: column;
  padding: var(--space-1);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

/* The popover card: the menu's surface, but sized to whatever it holds rather
   than to a list of rows — hence no min-width and no column flex (the Wire
   button's color picker is one row of eight swatches). */
.popup-popover {
  position: fixed;
  padding: var(--space-2);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.popup-menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  text-align: left;
  border: none;
  background: transparent;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: var(--font-size);
}

.popup-menu-item:enabled:hover,
.popup-menu-item--open {
  background: var(--color-surface-1);
}

.popup-menu-item:disabled {
  color: var(--color-muted);
  cursor: default;
}

.popup-menu-item--danger {
  color: var(--color-danger);
}

/* Optional per-item color dot (the wire / bus color pickers). */
.popup-menu-swatch {
  display: inline-block;
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border: 1px solid var(--color-overlay);
  border-radius: 50%;
}

/* Leading icon slot. Present but EMPTY on the iconless items of a card that
   has icons, so every label in that card still lines up. */
.popup-menu-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  color: var(--color-subtext);
}

.popup-menu-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Right-aligned shortcut hint ("⌘S") and the submenu chevron. */
.popup-menu-accel {
  flex: 0 0 auto;
  padding-left: var(--space-4);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}

.popup-menu-chevron {
  flex: 0 0 auto;
  padding-left: var(--space-3);
  color: var(--color-subtext);
}

/* A removable row (an Open Recent entry): the item plus its own × — a button
   can't nest another, so the two are siblings sharing one hover surface. */
.popup-menu-row {
  display: flex;
  align-items: center;
  border-radius: var(--radius-sm);
}

.popup-menu-row:hover {
  background: var(--color-surface-1);
}

.popup-menu-row .popup-menu-item {
  flex: 1 1 auto;
  min-width: 0;
}

.popup-menu-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-right: var(--space-2);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  opacity: 0;
}

/* The × stays out of the way until the row is hovered (or it is focused, so
   it remains reachable by keyboard). */
.popup-menu-row:hover .popup-menu-remove,
.popup-menu-remove:focus-visible {
  opacity: 1;
}

.popup-menu-remove:hover {
  background: var(--color-overlay);
  color: var(--color-text);
}

.popup-menu-separator {
  height: 1px;
  margin: var(--space-1) var(--space-2);
  background: var(--color-overlay);
}

/* ── Simulation live state (Feature 90) ──────────────────────────────────── */
/* While running the desk shows a plain arrow (edit affordances are gone). */
.desk-viewport--running {
  cursor: default;
}

/* Lit LED: full-opacity dome in its color + a glow halo. The dome carries
   its own --wire-color (set by the .part-led-dome--<color> class). Under
   Spice Lite, --led-level says how bright by its current: 1 at its
   datasheet's (the plain look), dimmer below it — never as dim as unlit's
   0.45 — and a wider halo past it. */
.part-discrete--lit .part-led-dome {
  fill-opacity: calc(0.5 + 0.5 * min(1, var(--led-level, 1)));
  filter: drop-shadow(0 0 calc(0.5px * var(--led-level, 1)) var(--wire-color))
    drop-shadow(0 0 calc(0.9px * var(--led-level, 1)) var(--wire-color));
}

/* A run toggling faster than the eye reads a glow (sim-overlay.js
   GLOW_MAX_HZ, 25 Hz): every lamp keeps its colour and brightness but drops
   the halo — switching a filter on and off re-layers the frame, and at that
   rate it was most of what the desk cost to draw. */
.desk-viewport--flat-lamps .part-discrete--lit .part-led-dome,
.desk-viewport--flat-lamps .part-seg--lit,
.desk-viewport--flat-lamps .part-clock--high .part-clock-lamp,
.desk-viewport--flat-lamps .part-discrete--on .part-transistor-lamp {
  filter: none;
}

/* Chip fault symbols (Feature 90): drawn over the body in pitch units, so they
   scale with the chip. A warning triangle when the chip has no power; the same
   red X + smoke a burnt LED gets when its polarity is reversed or 12 V killed
   it. Smoke rises past the viewBox, so let it escape. Feature 190 follow-up:
   the same triangle also flags a ROM/EPROM/EEPROM with no image loaded yet —
   computed from params alone, so it shows at design time, not just while
   running (see ChipView#refresh). */
.part-chip-svg {
  overflow: visible;
}

.part-chip--unpowered .part-warn,
.part-chip--unprogrammed .part-warn,
.part-chip--unsaved .part-warn,
.part-chip--timing .part-warn {
  display: block;
}

/* A timed part's readout (the 555, the RC-timed CD4000 parts): its rate or
   pulse, printed small under the part number while it runs — amber when the
   desk is drawing it slower (or a pulse longer) than true. */
.part-chip-timing {
  fill: var(--color-chip-label);
  font-family: var(--font-mono);
  font-size: 0.46px; /* SVG user units = pitch units */
  opacity: 0.9;
  user-select: none;
}

.part-chip--capped .part-chip-timing {
  fill: var(--color-warning);
  opacity: 1;
}

.part-chip--timing .part-chip-timing {
  opacity: 0.25;
}

.part-chip--reversed .part-burn,
.part-chip--damaged .part-burn,
.part-chip--overloaded .part-burn {
  display: block;
}

/* Spice Lite's brown smoke: the same burn, in a different smoke — the colour
   is not the only cue, the hover hint and the toast say which it was. */
.part-chip--overloaded .part-burn-smoke {
  fill: var(--color-part-smoke-brown);
}

/* The part number sits under the symbol — fade it rather than fight it. */
.part-chip--unpowered .part-chip-label,
.part-chip--reversed .part-chip-label,
.part-chip--damaged .part-chip-label,
.part-chip--overloaded .part-chip-label {
  opacity: 0.25;
}

/* Underpowered (3 V) is the one status without a symbol: it still computes
   nothing, but nothing is broken either — an amber corner dot suffices. */
.part-chip--underpowered::after {
  content: "";
  position: absolute;
  top: -3px;
  right: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1px solid var(--color-base);
  background: var(--color-warning);
}

/* Level-tinted net highlight while running (probe/inspector). Overrides the
   Feature 70 neutral glow via the level dataset on the SVG. */
.net-highlight[data-level="H"] .net-highlight-dot,
.net-highlight[data-level="H"] .net-highlight-pin,
.net-highlight[data-level="H"] .net-highlight-wire {
  fill: var(--color-sim-high);
  stroke: var(--color-sim-high);
}

.net-highlight[data-level="L"] .net-highlight-dot,
.net-highlight[data-level="L"] .net-highlight-pin,
.net-highlight[data-level="L"] .net-highlight-wire {
  fill: var(--color-sim-low);
  stroke: var(--color-sim-low);
}

.net-highlight[data-level="Z"] .net-highlight-dot,
.net-highlight[data-level="Z"] .net-highlight-pin,
.net-highlight[data-level="Z"] .net-highlight-wire {
  fill: var(--color-sim-float);
  stroke: var(--color-sim-float);
}

.net-highlight[data-level="X"] .net-highlight-dot,
.net-highlight[data-level="X"] .net-highlight-pin,
.net-highlight[data-level="X"] .net-highlight-wire {
  fill: var(--color-sim-conflict);
  stroke: var(--color-sim-conflict);
  animation: net-x-flash 0.7s steps(1) infinite;
}

@keyframes net-x-flash {
  50% {
    fill-opacity: 0.2;
    stroke-opacity: 0.2;
  }
}

.net-status[data-level="H"] {
  border-color: var(--color-sim-high);
}

.net-status[data-level="L"] {
  border-color: var(--color-sim-low);
}

.net-status[data-level="Z"] {
  border-color: var(--color-sim-float);
}

.net-status[data-level="X"] {
  border-color: var(--color-sim-conflict);
}

/* ── Notification toast stack (Feature 90) ───────────────────────────────── */
.toast-stack {
  position: fixed;
  top: calc(var(--header-height) + var(--space-3));
  right: var(--space-4);
  z-index: 40;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 320px;
  pointer-events: none;
}

/* A toast past the shown few waits behind the "+N more" line. */
.toast.toast--overflow {
  display: none;
}

.toast-more {
  align-self: flex-end;
  pointer-events: auto;
  padding: 2px var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-subtext);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.toast-more[hidden] {
  display: none;
}

.toast {
  pointer-events: auto;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-1);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.toast--info {
  border-left-color: var(--color-info);
}

.toast--warning {
  border-left-color: var(--color-warning);
}

.toast--danger {
  border-left-color: var(--color-danger);
}

.toast-title {
  font-weight: 600;
  margin-bottom: 2px;
}

.toast-message {
  color: var(--color-subtext);
}

/* The one button a toast may carry, for a toast that OFFERS something rather
   than only saying it ("Update ready" → Restart). Deliberately quiet — a
   bordered chip, not a filled primary — because it appears unbidden in the
   corner of the desk, and nothing arriving that way should look like the
   next thing to click. Same shape as `.settings-action`, one size down. */
.toast-action {
  display: inline-flex;
  align-items: center;
  margin-top: var(--space-2);
  padding: 3px var(--space-2);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-text);
  font-size: var(--font-size-xs);
  font-weight: 500;
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    color 0.15s ease;
}

.toast-action:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* ── Chip pin-assignments dialog (wiring reference) ──────────────────────── */
.chip-pinout {
  min-width: 380px;
  max-width: 460px;
}

/* The pinout header carries the title and, top-right, up to two line-drawn
   buttons: "open the example circuit" (shown when this part has a bundled
   demonstration bench) and "open the datasheet PDF" (shown when the user's
   datasheet folder holds a <id>.pdf for this part). ONE rule, one box — they
   are the same affordance with different glyphs, and `.popup-header`'s own row
   layout right-aligns however many of them there are. */
.pinout-header-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease,
    border-color 0.15s ease;
}

.pinout-header-btn:hover {
  background: var(--color-surface-1);
  color: var(--color-accent);
  border-color: var(--color-accent);
}

.pinout-header-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.chip-pinout-sub {
  padding: 2px var(--space-4) var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

/* The manufacturer-datasheet crop (connection diagram / function table),
   shown below the pin map in the standalone pinout window. The crop PNG has a
   white background baked in, so it sits on a light card that reads as a paper
   excerpt against the dark dialog; it removes itself when there is no crop. */
.chip-pinout-datasheet {
  margin: 0;
  padding: var(--space-3) var(--space-4) var(--space-4);
  border-top: 1px solid var(--color-crust);
}

.chip-pinout-datasheet-cap {
  padding-bottom: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.chip-pinout-datasheet-img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  margin: 0 auto;
  padding: var(--space-2);
  background: #fff;
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
}

.chip-pinout-dip {
  position: relative;
  display: flex;
  justify-content: center;
  padding: var(--space-4);
}

/* The DIP notch cue, cut into the top edge of the outlined body. */
.chip-pinout-notch {
  position: absolute;
  top: calc(var(--space-4) - 1px);
  left: 50%;
  transform: translateX(-50%);
  width: 18px;
  height: 9px;
  background: var(--color-surface-0);
  border: 2px solid var(--color-chip-leg);
  border-top: none;
  border-radius: 0 0 10px 10px;
  z-index: 1;
}

.chip-pinout-grid {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 344px;
}

.chip-pinout-row {
  display: grid;
  grid-template-columns: 1fr 52px 1fr;
  align-items: stretch;
}

/* The center strip reads as the chip body running down the middle — a
   light 2px outline lifts the near-black body off the dark dialog surface
   (a hard visibility win for low-vision users) and a subtle top-light
   gradient gives it a molded-plastic feel. */
.chip-pinout-body {
  position: relative;
  background: linear-gradient(
    var(--color-surface-1),
    var(--color-chip-body) 55%
  );
  border-left: 2px solid var(--color-chip-leg);
  border-right: 2px solid var(--color-chip-leg);
}

.chip-pinout-row:first-child .chip-pinout-body {
  border-top: 2px solid var(--color-chip-leg);
  border-radius: 6px 6px 0 0;
}

.chip-pinout-row:last-child .chip-pinout-body {
  border-bottom: 2px solid var(--color-chip-leg);
  border-radius: 0 0 6px 6px;
}

/* Pin-1 dot inside the body's top-left corner (datasheet orientation cue). */
.chip-pinout-row:first-child .chip-pinout-body::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 8px;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-chip-leg);
}

.chip-pinout-pin {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 3px var(--space-1);
}

.chip-pinout-pin--right {
  justify-content: flex-end;
}

.chip-pinout-num {
  flex: 0 0 auto;
  min-width: calc(var(--font-size) + 7px);
  height: calc(var(--font-size) + 5px);
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-subtext);
  background: var(--color-surface-1);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
}

.chip-pinout-name {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}

.chip-pinout-label {
  font-size: var(--font-size);
  font-weight: 600;
  color: var(--color-text);
}

.chip-pinout-role {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
  text-transform: uppercase;
}

.chip-pinout-name--vcc .chip-pinout-label {
  color: var(--color-rail-plus);
}

.chip-pinout-name--gnd .chip-pinout-label {
  color: var(--color-rail-minus);
}

.chip-pinout-name--output .chip-pinout-label {
  color: var(--color-sim-high);
}

.chip-pinout-name--nc .chip-pinout-label {
  color: var(--color-muted);
  font-weight: 400;
}

.chip-pinout-name--anode .chip-pinout-label {
  color: var(--color-rail-plus);
}

.chip-pinout-name--cathode .chip-pinout-label {
  color: var(--color-rail-minus);
}

/* Single-column list layout for discretes (linear footprint) + desk bricks
   (terminals) — each line is a badge, a role-colored name, and a detail. */
.part-pinout-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.part-pinout-line {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) 0;
}

.part-pinout-detail {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* ── Build guide (Feature 140) ────────────────────────────────────────────── */
/* A right-docked panel formatting the pure plan from model/build-plan.js: the
   BOM, the net-grouped wiring list, and the ordered assembly steps. */
.build-guide {
  display: flex;
  flex-direction: column;
  width: 300px;
  flex: 0 0 auto;
  background: var(--color-mantle);
  border-left: 1px solid var(--color-crust);
  min-height: 0;
}

.build-guide[hidden] {
  display: none;
}

.build-guide-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-crust);
}

.build-guide-header-left {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.build-guide-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
}

/* Download-the-BOM button, sitting beside the title. */
.build-guide-download {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
}

.build-guide-download:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* The warning count beside the title. */
.build-guide-warn-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--font-size) + 5px);
  height: calc(var(--font-size) + 5px);
  padding: 0 5px;
  border-radius: 999px;
  background: var(--color-warning);
  color: var(--color-crust);
  font-size: var(--font-size-xs);
  font-weight: 700;
}

.build-guide-warn-badge[hidden] {
  display: none;
}

.build-guide-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--segment-height);
  height: var(--segment-height);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-size: var(--font-size-xl);
  line-height: 1;
  cursor: pointer;
}

.build-guide-close:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* Tab strip — shared with Generate's file viewer (.code-files-tabs), which is
   the same control on a card. */
.build-guide-tabs,
.code-files-tabs {
  display: flex;
  border-bottom: 1px solid var(--color-crust);
}

.build-guide-tab,
.code-files-tab {
  flex: 1 1 0;
  padding: var(--space-2) 0;
  border: none;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--color-muted);
  font: inherit;
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.build-guide-tab:hover,
.code-files-tab:hover {
  color: var(--color-text);
}

.build-guide-tab[aria-selected="true"],
.code-files-tab[aria-selected="true"] {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

.build-guide-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-3);
  font-size: var(--font-size-sm);
}

/* Section headers, shared by BOM + Steps (mirrors the palette group heads). */
.build-guide-section {
  margin-bottom: var(--space-4);
}

.build-guide-section-head {
  margin: 0 0 var(--space-2);
  font-size: var(--font-size-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-muted);
}

/* BOM lines. */
.build-guide-bom-list,
.build-guide-steps,
.build-guide-step-detail {
  margin: 0;
  padding: 0;
  list-style: none;
}

.build-guide-bom-line {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 3px 0;
}

/* The part title takes the slack, so the count sits hard right whether or not
   the row carries an item number. */
.build-guide-bom-title {
  flex: 1;
}

/* A wire line's BOM ITEM NUMBER — the callout every wire step quotes
   (build-plan.js's wireItemLabel). Mono and fixed-width-ish so a column of them
   reads down the list; the accent marks it as a cross-reference rather than part
   of the name. */
.build-guide-bom-item {
  font-family: var(--font-mono);
  color: var(--color-accent);
  white-space: nowrap;
}

.build-guide-count {
  font-family: var(--font-mono);
  color: var(--color-muted);
  white-space: nowrap;
}

/* Steps. */
.build-guide-step {
  padding: 3px 0;
}

.build-guide-step-main {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  cursor: pointer;
}

.build-guide-step-check {
  margin-top: 2px;
  flex: 0 0 auto;
}

.build-guide-step--done .build-guide-step-text {
  text-decoration: line-through;
  color: var(--color-muted);
}

.build-guide-step-detail {
  margin: 2px 0 4px calc(var(--space-4) + 2px);
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

.build-guide-step-detail li {
  padding: 1px 0;
}

/* Warnings banner (shown on every tab). */
.build-guide-warnings {
  margin-bottom: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid color-mix(in srgb, var(--color-warning) 45%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
}

.build-guide-warnings-head {
  font-weight: 600;
  margin-bottom: var(--space-1);
}

.build-guide-warn-list {
  margin: 0;
  padding-left: var(--space-4);
}

.build-guide-warn {
  padding: 1px 0;
}

.build-guide-empty {
  color: var(--color-muted);
  text-align: center;
  margin-top: var(--space-5);
}

/* ── Schematic view (Feature 150) ─────────────────────────────────────────
   A second pannable/zoomable surface (its own DeskView) drawing the SAME
   document as chip symbols + routed nets. Coordinates inside .schematic-svg
   are pitch units (viewBox), so strokes/offsets below are in pitch too. */
.schematic-viewport {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  background-color: var(--color-base);
  background-image: radial-gradient(
    circle,
    var(--color-desk-dot) 1px,
    transparent 1.5px
  );
  cursor: default;
}

.schematic-viewport[hidden] {
  display: none;
}

/* The svg spans the diagram bounds; empty area is click-through so dragging
   bare desk still pans (only symbols are interactive). */
.schematic-svg {
  position: absolute;
  overflow: visible;
  pointer-events: none;
}

.schematic-node {
  pointer-events: all; /* inherited by the box/labels/stubs within */
  cursor: grab;
}

.schematic-box {
  fill: var(--color-surface-0);
  stroke: var(--color-subtext);
  stroke-width: 0.14;
}

.schematic-label {
  fill: var(--color-text);
  font-family: var(--font-mono);
  font-weight: 600;
  user-select: none;
}

.schematic-pin-label {
  fill: var(--color-subtext);
  font-family: var(--font-sans);
  user-select: none;
}

.schematic-stub {
  stroke: var(--color-subtext);
  stroke-width: 0.1;
}

.schematic-wire {
  fill: none;
  stroke: var(--color-subtext);
  stroke-width: 0.12;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.schematic-edge--bus .schematic-wire {
  stroke: var(--color-text);
  stroke-width: 0.36;
}

.schematic-edge--dangling .schematic-wire {
  stroke-dasharray: 0.4 0.3;
}

.schematic-net-label {
  fill: var(--color-info);
  font-family: var(--font-mono);
  font-weight: 600;
  user-select: none;
}

/* Distinctive-shape gate glyph — a subtle type badge in the box corner. */
.schematic-glyph-shape {
  fill: var(--color-overlay);
  stroke: var(--color-subtext);
  stroke-width: 0.05;
}

.schematic-glyph-line {
  fill: none;
  stroke: var(--color-subtext);
  stroke-width: 0.06;
}

/* Power / ground rail symbols dropped at each supply pin (never routed). */
/* External-signal stub (Feature 370): the same flag the desk draws, in the
   signal's own colour, saying which net a bench button is driving. */
.schematic-signal-stem {
  stroke: var(--signal-color);
  stroke-width: 0.08;
}

.schematic-signal-flag {
  fill: var(--signal-color);
  fill-opacity: 0.35;
  stroke: var(--signal-color);
  stroke-width: 0.08;
  stroke-linejoin: round;
}

.schematic-signal-label {
  fill: var(--signal-color);
}

.schematic-power-stem,
.schematic-power-bar {
  fill: none;
  stroke: var(--color-text);
  stroke-width: 0.12;
  stroke-linecap: round;
}

.schematic-power--VCC .schematic-power-stem,
.schematic-power--VCC .schematic-power-bar {
  stroke: var(--color-wire-red);
}

.schematic-power-label {
  fill: var(--color-muted);
  font-family: var(--font-mono);
  user-select: none;
}

/* ── Live simulation tint (running) — level colors match the breadboard ── */
.schematic-edge[data-level="H"] .schematic-wire,
.schematic-stub[data-level="H"] {
  stroke: var(--color-sim-high);
}

.schematic-edge[data-level="L"] .schematic-wire,
.schematic-stub[data-level="L"] {
  stroke: var(--color-sim-low);
}

.schematic-edge[data-level="Z"] .schematic-wire,
.schematic-stub[data-level="Z"] {
  stroke: var(--color-sim-float);
}

.schematic-edge[data-level="X"] .schematic-wire,
.schematic-stub[data-level="X"] {
  stroke: var(--color-sim-conflict);
  animation: net-x-flash 0.7s steps(1) infinite;
}

/* ── Shared probe highlight — same net lit in both views ── */
.schematic-edge.schematic--highlight .schematic-wire {
  stroke: var(--color-net-glow);
  stroke-width: 0.32;
}

.schematic-stub.schematic--highlight {
  stroke: var(--color-net-glow);
  stroke-width: 0.24;
}

/* ── Chip health badges (running) ── */
.schematic-node--damaged .schematic-box,
.schematic-node--overloaded .schematic-box,
.schematic-node--reversed .schematic-box {
  stroke: var(--color-danger);
  stroke-width: 0.24;
}

.schematic-node--underpowered .schematic-box {
  stroke: var(--color-warning);
  stroke-width: 0.2;
}

.schematic-node--unpowered .schematic-box {
  stroke: var(--color-muted);
  stroke-dasharray: 0.4 0.3;
}

.schematic-hint {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--color-muted);
  font-size: var(--font-size-lg);
  user-select: none;
  pointer-events: none;
}

.schematic-hint[hidden] {
  display: none;
}

/* ── 3D view (the toolbar's cube segment) ────────────────────────────────
   A sibling of the desk and the schematic, filling the stage when it is the
   view showing. The canvas is the whole surface; the hint sits along the
   bottom edge, pointer-inert, so the canvas takes every gesture. */
.desk3d-viewport {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  background-color: var(--color-base);
  cursor: grab;
}

.desk3d-viewport[hidden] {
  display: none;
}

.desk3d-viewport--dragging {
  cursor: grabbing;
}

.desk3d-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  touch-action: none;
}

.desk3d-hint {
  position: absolute;
  left: var(--space-3);
  bottom: var(--space-3);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--color-surface-0) 80%, transparent);
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
  user-select: none;
  pointer-events: none;
}

.desk3d-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--space-3);
  color: var(--color-muted);
  font-size: var(--font-size-lg);
  text-align: center;
  user-select: none;
}

.desk3d-hint[hidden],
.desk3d-fallback[hidden] {
  display: none;
}

.schematic-tools {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: flex;
  gap: var(--space-2);
}

.schematic-tool-btn {
  height: var(--control-height);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-0);
  color: var(--color-text);
  font-size: var(--font-size);
  cursor: pointer;
}

.schematic-tool-btn:hover {
  background: var(--color-surface-1);
}

/* ── Distinctive-shape parts (Feature 150 discretes) ─────────────────────── */
.schematic-shape-line {
  fill: none;
  stroke: var(--color-subtext);
  stroke-width: 0.14;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.schematic-shape-body {
  fill: var(--color-surface-0);
  stroke: var(--color-subtext);
  stroke-width: 0.14;
}

.schematic-shape-dot {
  fill: var(--color-subtext);
}

/* A potentiometer's wiper arrowhead, in the line colour. */
.schematic-shape-arrow {
  fill: var(--color-subtext);
}

.schematic-led-body {
  fill: var(--led-color, var(--color-wire-red));
  fill-opacity: 0.32;
  stroke: var(--color-subtext);
  stroke-width: 0.12;
}

.schematic-led-bar {
  stroke: var(--color-subtext);
  stroke-width: 0.16;
  stroke-linecap: round;
}

.schematic-led-ray {
  stroke: var(--color-subtext);
  stroke-width: 0.1;
  stroke-linecap: round;
}

.schematic-shape--led.schematic-lit .schematic-led-body {
  fill-opacity: 1;
}

.schematic-shape--led.schematic-lit .schematic-led-ray {
  stroke: var(--led-color, var(--color-wire-red));
  stroke-width: 0.18;
}

.schematic-shape-label {
  fill: var(--color-text);
  font-family: var(--font-mono);
  font-weight: 600;
  user-select: none;
}

.schematic-shape-value {
  fill: var(--color-muted);
  font-family: var(--font-mono);
  user-select: none;
}

.schematic-terminal-label {
  fill: var(--color-muted);
  font-family: var(--font-sans);
  user-select: none;
}

/* ── Logic analyzer (Feature 210) ────────────────────────────────────────────
   A bottom-docked waveform panel: a header, a channel gutter, and a
   horizontally-scrolling SVG lanes surface. Colors come from the sim-level
   tokens so waveforms match the desk/schematic tinting. */
.scope-panel {
  display: flex;
  flex-direction: column;
  height: 280px;
  max-height: 50vh;
  flex: 0 0 auto;
  background: var(--color-mantle);
  border-top: 1px solid var(--color-crust);
  min-height: 0;
}

.scope-panel[hidden] {
  display: none;
}

/* The draggable top edge. A dedicated flex strip on the seam — it grows the
   panel upward (capped at half the window by the renderer). */
.scope-resize {
  position: relative;
  flex: 0 0 auto;
  height: 6px;
  cursor: ns-resize;
  touch-action: none;
}

.scope-resize::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 3px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-sm);
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity 0.12s ease;
}

.scope-resize:hover::after,
.scope-resize--active::after {
  opacity: 1;
}

.scope-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--color-crust);
  flex: 0 0 auto;
}

.scope-title {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  flex: 0 0 auto;
}

.scope-tools {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 1 1 auto;
}

.scope-sep {
  width: 1px;
  height: 16px;
  background: var(--color-overlay);
  margin: 0 var(--space-1);
}

.scope-delta {
  margin-left: auto;
  font: var(--font-size-xs) / 1 var(--font-mono);
  color: var(--color-subtext);
  white-space: nowrap;
}

.scope-btn {
  padding: 2px var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-text);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.scope-btn:hover {
  background: var(--color-surface-1);
}

.scope-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

/* Fit while the scale is fitting the trace (no scale of the user's chosen). */
.scope-btn--active,
.scope-btn--active:hover {
  background: var(--color-surface-1);
  border-color: var(--color-subtext);
}

/* The Timing | Transitions picker: the app's shared segmented track, packed
   beside the title as the AI panel's Build/Review is. */
.scope-axis {
  display: flex;
  flex: 0 0 auto;
}

/* Timing mode's scale: − / "1 ms/div" / + / Fit. */
.scope-zoom {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.scope-zoom[hidden] {
  display: none;
}

.scope-zoom-btn {
  min-width: calc(var(--font-size) + 9px);
  padding: 2px 0;
}

.scope-per-div {
  min-width: 9ch;
  font: var(--font-size-xs) / 1 var(--font-mono);
  color: var(--color-subtext);
  text-align: center;
  white-space: nowrap;
}

.scope-close {
  flex: 0 0 auto;
  width: var(--segment-height);
  height: var(--segment-height);
  padding: 0;
  font-size: var(--font-size-xl);
  line-height: 1;
  color: var(--color-muted);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.scope-close:hover {
  color: var(--color-text);
  background: var(--color-surface-0);
}

.scope-body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

.scope-gutter {
  width: 210px;
  flex: 0 0 auto;
  overflow: hidden;
  border-right: 1px solid var(--color-crust);
  background: var(--color-mantle);
  user-select: none;
  touch-action: none;
}

/* A row is its own drag handle: grab it to move the channel up or down. */
.scope-chan {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0 var(--space-2);
  box-sizing: border-box;
  border-bottom: 1px solid var(--color-crust);
  cursor: grab;
}

/* The held row floats over its neighbours, under the pointer. */
.scope-chan--dragging {
  position: relative;
  z-index: 1;
  background: var(--color-surface-0);
  box-shadow: var(--shadow-1);
}

/* Through the whole drag, wherever the pointer is over the panel — doubled to
   outrank the buttons' pointer and the lanes' crosshair. */
.scope-panel.scope-panel--reordering,
.scope-panel.scope-panel--reordering * {
  cursor: grabbing;
}

/* The held channel's lane, shaded in the row it would land in. */
.scope-lane-lift {
  fill: var(--color-surface-0);
}

.scope-chan-dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex: 0 0 auto;
}

.scope-chan-name {
  flex: 1 1 auto;
  font-size: var(--font-size-xs);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.scope-chan-value {
  font: var(--font-size-xs) / 1 var(--font-mono);
  color: var(--color-subtext);
  min-width: 4ch;
  text-align: right;
}

.scope-chan-ctl {
  display: flex;
  gap: 2px;
  flex: 0 0 auto;
}

.scope-mini {
  width: calc(var(--font-size) + 5px);
  height: calc(var(--font-size) + 5px);
  padding: 0;
  font-size: var(--font-size-xs);
  line-height: 1;
  color: var(--color-muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.scope-mini:hover:not(:disabled) {
  color: var(--color-text);
  background: var(--color-surface-0);
}

.scope-mini:disabled {
  opacity: 0.3;
  cursor: default;
}

.scope-mini--del:hover:not(:disabled) {
  color: var(--color-danger);
}

.scope-lanes {
  flex: 1 1 auto;
  overflow: auto;
  background: var(--color-base);
  cursor: crosshair;
}

/* The lanes' whole width and height — what the panel scrolls. Only the
   stretch on screen is drawn: the svg is that window, placed at its x. */
.scope-track {
  position: relative;
}

.scope-svg {
  display: block;
  position: absolute;
  top: 0;
}

.scope-empty {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex-wrap: wrap;
  padding: var(--space-4);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
  text-align: center;
}

.scope-empty[hidden] {
  display: none;
}

.scope-body[hidden] {
  display: none;
}

.scope-link {
  padding: 0;
  font: inherit;
  color: var(--color-info);
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
}

/* ── AI circuit builder (Feature 260) ────────────────────────────────────────
   A bottom-docked conversation panel sharing the analyzer's shell — same
   header, same draggable top edge, same docking — because they are the same
   kind of thing: a workspace that borrows the desk's bottom edge. What differs
   is the body: a scrolling transcript over a compose row. */
.ai-panel {
  display: flex;
  flex-direction: column;
  height: 280px;
  max-height: 60vh;
  flex: 0 0 auto;
  background: var(--color-mantle);
  border-top: 1px solid var(--color-crust);
  min-height: 0;
}

.ai-panel[hidden] {
  display: none;
}

.ai-resize {
  position: relative;
  flex: 0 0 auto;
  height: 6px;
  cursor: ns-resize;
  touch-action: none;
}

.ai-resize::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 3px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-sm);
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity 0.12s ease;
}

.ai-resize:hover::after,
.ai-resize--active::after {
  opacity: 1;
}

.ai-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--color-crust);
  flex: 0 0 auto;
}

.ai-title {
  font-weight: 600;
  font-size: var(--font-size-sm);
  color: var(--color-text);
  flex: 0 0 auto;
}

/* The Build/Review picker (Feature 320). It holds the app's shared segmented
   track, so there is nothing to style but its place in the header: fixed width,
   left-packed beside the title, with .ai-tools the elastic member that pushes
   the status and the close button right. */
.ai-mode {
  display: flex;
  flex: 0 0 auto;
}

.ai-tools {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
  flex: 1 1 auto;
}

.ai-status {
  font: var(--font-size-xs) / 1 var(--font-mono);
  color: var(--color-subtext);
  white-space: nowrap;
}

/* The session's running token total. A SIBLING of `.ai-status` rather than a
   second writer of it — `#setBusy` owns the status outright — and one step more
   muted, because the status is about what is happening now and this is a total
   the Clear button resets. */
.ai-usage {
  font: var(--font-size-xs) / 1 var(--font-mono);
  color: var(--color-muted);
  white-space: nowrap;
}

.ai-btn {
  padding: 2px var(--space-2);
  font-size: var(--font-size-xs);
  color: var(--color-text);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.ai-btn:hover {
  background: var(--color-surface-1);
}

.ai-close {
  flex: 0 0 auto;
  width: var(--segment-height);
  height: var(--segment-height);
  padding: 0;
  font-size: var(--font-size-xl);
  line-height: 1;
  color: var(--color-muted);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.ai-close:hover {
  color: var(--color-text);
  background: var(--color-surface-0);
}

/* The transcript. Rows are typed by what they report, and the type shows as a
   left rule + text colour rather than as a chat bubble — this is a build log,
   not a messaging app. */
.ai-log {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-2) var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ai-row {
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-overlay);
}

.ai-row-text {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-subtext);
  white-space: pre-wrap;
}

.ai-row-list {
  margin: var(--space-1) 0 0;
  padding-left: var(--space-4);
  font-size: var(--font-size-xs);
  line-height: 1.5;
  color: var(--color-muted);
}

/* What the user asked for reads as the strongest line in the log. */
.ai-row--you {
  border-left-color: var(--color-accent);
}

.ai-row--you .ai-row-text {
  color: var(--color-text);
  font-weight: 500;
}

.ai-row--ok {
  border-left-color: var(--color-success);
}

.ai-row--ok .ai-row-text {
  color: var(--color-text);
}

.ai-row--fail {
  border-left-color: var(--color-danger);
}

.ai-row--fail .ai-row-text {
  color: var(--color-danger);
}

.ai-row--working .ai-row-text {
  font-style: italic;
}

/* A review's answer (Feature 320). The one row whose CONTENT is the point
   rather than a status about it, so it reads at full text colour — but with no
   accent rule, because the accent marks what the USER said. */
.ai-row--reply .ai-row-text {
  color: var(--color-text);
}

/* The compose row: a growable prompt beside a single send/cancel button. */
.ai-compose {
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  flex: 0 0 auto;
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-crust);
}

.ai-input {
  flex: 1 1 auto;
  min-width: 0;
  box-sizing: border-box;
  resize: none;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-0);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}

.ai-input:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.ai-input:disabled {
  color: var(--color-muted);
}

.ai-send {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  color: var(--color-text);
  background: var(--color-surface-0);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.ai-send:hover {
  border-color: var(--color-accent);
}

/* Busy is the SAME button, meaning cancel — one control for one activity. */
.ai-send--busy {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

/* The place-signal ghost: a pill riding the cursor in the pointer-inert
   overlay, saying what a click would add. It is deliberately NOT a flag at a
   hole — a signal has no desk position, so the click point is discarded and
   the button goes to the rail. World-layer text, so its size is a raw px
   (see .annotation-text, and tests/type-scale.test.js). */
.signal-place-ghost {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px;
  background: var(--color-surface-0);
  border: 1px dashed var(--color-accent);
  border-radius: var(--radius-sm);
  color: var(--color-subtext);
  font-size: 12px; /* world/SVG user units — the zoom-scaled layer */
  white-space: nowrap;
  pointer-events: none;
}

.signal-place-ghost-mark {
  color: var(--color-accent);
}

/* ── Signal rail (Feature 370) ───────────────────────────────────────────── */
/* The external-signal buttons, pinned down the viewport's right edge between
   the padlock (top) and the zoom cluster (bottom). A sibling of .desk-surface,
   which DeskView prepends, so it paints above the desk with no z-index. */
.signal-rail {
  position: absolute;
  right: var(--space-3);
  top: calc(var(--space-2) + var(--desk-lock-size) + var(--space-2));
  bottom: calc(var(--space-4) + var(--desk-zoom-height) + var(--space-2));
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  overflow: hidden;
  /* The COLUMN is spacing, not a control: ten rows down the right edge must
     not turn a tall strip of desk into dead space. Its ROWS take the clicks. */
  pointer-events: none;
}

/* The signal rows get their own list, so the column can carry the serial
   integration's element cards below them (.integration-rail). */
.signal-rail-list {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
}

.signal-rail-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  pointer-events: auto;
}

/* The unplaced flag, waiting beside its button. Gone once it is on the desk —
   the flag is in one place at a time, and that is the whole state readout. */
.signal-flag-chip {
  display: grid;
  place-items: center;
  width: 28px;
  height: 16px;
  cursor: grab;
}

.signal-rail-row--placed .signal-flag-chip {
  visibility: hidden;
}

.signal-flag-chip-art {
  width: 100%;
  height: 100%;
  overflow: visible;
}

.signal-flag-chip-poly {
  fill: var(--signal-color);
  fill-opacity: var(--signal-flag-alpha);
  stroke: var(--signal-color);
  stroke-width: 0.16;
  stroke-linejoin: round;
}

.signal-btn {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 140px;
  height: calc(var(--font-size) + 11px);
  padding: 0 var(--space-2);
  background: var(--color-surface-0);
  border: 1px solid var(--color-crust);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

/* The dot is the link back to this signal's flag on the desk: the two share
   a colour, and the dot carries the digit KEY the flag prints — colours
   repeat, keys do not. The digit is a fixed dark ink rather than a theme
   token, because it sits on the signal's own colour, which is a mid-to-light
   tone in both themes. */
.signal-btn-dot {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--font-size-xs) + 5px);
  height: calc(var(--font-size-xs) + 5px);
  border-radius: 50%;
  background: var(--signal-color);
  /* The wires' own under-stroke, so a WHITE dot keeps an edge on a light rail. */
  box-shadow: inset 0 0 0 1px var(--color-wire-outline);
  color: var(--color-wire-black);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.signal-btn-label {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* A latching button reads differently from one held down: it keeps a ring even
   at rest, so "this one stays where you put it" is visible before you press. */
.signal-btn--toggle {
  border-style: dashed;
}

.signal-btn--on {
  background: var(--signal-color);
  border-color: var(--signal-color);
  color: var(--color-crust);
}

/* Held: the button fills with the colour, so the dot turns inside out — a
   dark disc with the digit in the colour. */
.signal-btn--on .signal-btn-dot {
  background: var(--color-wire-black);
  color: var(--signal-color);
}

/* Its flag is selected on the desk: the button takes the selected flag's own
   outline colour, so the pair reads as one pick. */
.signal-btn--selected {
  border-color: var(--color-selection, var(--color-accent));
  box-shadow: 0 0 0 1px var(--color-selection, var(--color-accent));
}

.signal-btn:hover {
  color: var(--color-text);
}

.signal-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* ── Output / Input cards (the Arduino serial integration) ──────────────── */
/* The elements' home on the rail, under the signal buttons in the same
   column: a head naming the element, and under it the tags still waiting to
   go on the board, as chips to drag. A fully wired element is a head alone. */
.integration-rail {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.integration-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
  max-width: 180px;
  padding: var(--space-1);
  background: var(--color-surface-0);
  border: 1px solid var(--color-crust);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
  pointer-events: auto;
}

.integration-card--selected {
  border-color: var(--color-selection, var(--color-accent));
  box-shadow: 0 0 0 1px var(--color-selection, var(--color-accent));
}

.integration-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: calc(var(--font-size) + 7px);
  padding: 0 var(--space-1);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
  /* No pointer: a primary click does nothing here — the head answers only a
     right-click (the element's menu), which the hover wash still marks. */
  cursor: default;
}

.integration-card-head:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.integration-card-head:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.integration-card-dir {
  color: var(--color-muted);
  font-weight: 700;
}

.integration-card-dot {
  flex: 0 0 auto;
  width: calc(var(--font-size-xs) - 1px);
  height: calc(var(--font-size-xs) - 1px);
  border-radius: 50%;
  background: var(--signal-color);
  box-shadow: inset 0 0 0 1px var(--color-wire-outline);
}

.integration-card-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-align: left;
}

.integration-card-width {
  flex: 0 0 auto;
  color: var(--color-muted);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

/* No connection chosen yet: the head says so in its tooltip, and the card
   wears the warning colour so it is found before Run refuses. */
.integration-card--unassigned .integration-card-name {
  color: var(--color-warning);
}

.integration-card-tags {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 2px;
}

.integration-tag-chip {
  display: inline-grid;
  place-items: center;
  min-width: calc(var(--font-size-xs) + 6px);
  height: calc(var(--font-size-xs) + 6px);
  padding: 0 2px;
  box-sizing: border-box;
  border: 1px solid var(--signal-color);
  border-radius: 3px;
  background: color-mix(
    in srgb,
    var(--signal-color) calc(var(--signal-flag-alpha) * 100%),
    transparent
  );
  color: var(--color-text);
  font-size: var(--font-size-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  cursor: grab;
  user-select: none;
}

.integration-tag-chip--trigger {
  border-style: dashed;
}

/* ── The width asked for when an element is dropped ─────────────────────── */
.integration-width-popover {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
}

.integration-width-title {
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
}

.integration-width-options {
  display: flex;
  gap: var(--space-2);
}

.integration-width-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: calc(var(--font-size) + 13px);
  height: calc(var(--font-size) + 13px);
  padding: 0 4px;
  box-sizing: border-box;
  border: 1px solid var(--color-overlay);
  border-radius: 4px;
  background: var(--color-surface-1);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: transform 0.15s ease;
}

.integration-width-option:hover {
  transform: scale(1.1);
}

.integration-width-option:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ── Activity lamps (TX · RX · LG) ───────────────────────────────────────── */
/* The zoom cluster's pill, fixed immediately to its LEFT: the same height
   (--desk-zoom-height), and a right edge one gap past the cluster's own
   width (--desk-zoom-width), which the cluster itself is sized by. */
.integration-lamps {
  position: absolute;
  bottom: var(--space-4);
  right: calc(var(--space-4) + var(--desk-zoom-width) + var(--space-2));
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--desk-zoom-height);
  box-sizing: border-box;
  padding: var(--space-1) var(--space-2);
  background: var(--color-surface-0);
  border: 1px solid var(--color-crust);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.integration-lamps[hidden] {
  display: none;
}

.integration-lamp {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0 var(--space-1);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-weight: 700;
}

button.integration-lamp {
  cursor: pointer;
}

button.integration-lamp:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.integration-lamp-led {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-surface-1);
  box-shadow: inset 0 0 0 1px var(--color-overlay);
  transition: background-color 0.08s linear;
}

.integration-lamp--tx.integration-lamp--on .integration-lamp-led {
  background: var(--color-sim-high);
  box-shadow: 0 0 5px var(--color-sim-high);
}

.integration-lamp--rx.integration-lamp--on .integration-lamp-led {
  background: var(--color-info);
  box-shadow: 0 0 5px var(--color-info);
}

.integration-lamp--lg.integration-lamp--on .integration-lamp-led {
  background: var(--color-warning);
  box-shadow: 0 0 5px var(--color-warning);
}

.integration-lamp--on {
  color: var(--color-text);
}

/* ── The Generate segment's out-of-sync dot ──────────────────────────────── */
.toolbar-generate {
  position: relative;
}

.toolbar-stale-dot {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--color-warning);
  box-shadow: 0 0 0 1px var(--color-mantle);
  pointer-events: none;
}

.toolbar-stale-dot[hidden] {
  display: none;
}

/* ── Settings ▸ Serial I/O (integration-settings.js) ─────────────────────── */
/* The picker: a `.settings-row` (its padding, and the containing block for
   the (i)'s note card) holding the connection <select> filling the row, then
   + and the bin as bare glyphs — the shape Rest Hippo's environment bar uses —
   then the (i). Glyph buttons sit closer together than a row's label and
   control do. */
.integration-picker {
  gap: var(--space-1);
}

.integration-picker-select {
  flex: 1 1 auto;
  min-width: 0;
}

.integration-picker .info-btn {
  margin-left: var(--space-1);
}

.integration-picker-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-subtext);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.integration-picker-btn:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.integration-picker-btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The bin reddens under the pointer, and STAYS red while armed — its second
   click removes. Both after the base rules: same specificity, so source order
   is what makes red win. */
.integration-picker-btn--danger:hover,
.integration-picker-btn--armed {
  color: var(--color-danger);
}

/* The Mock is picked: there is nothing to remove. */
.integration-picker-btn:disabled {
  opacity: 0.35;
  cursor: default;
  background: transparent;
  color: var(--color-subtext);
}

/* The editor for the picked connection: plain `.settings-row`s on the panel,
   spaced and lettered as every other tab's are, with no card around them. A
   problem is marked where it is — on the Port field, and in the connection's
   name in the picker. */
.integration-conn {
  display: flex;
  flex-direction: column;
}

/* A field row's select (and the Port's select + Re-scan pair) fills the
   field column, as `.settings-row--field`'s text input does. */
.integration-field > .settings-select,
.integration-port-row {
  flex: 1 1 0;
  min-width: 0;
}

.integration-field--problem .settings-label {
  color: var(--color-danger);
}

.integration-field--problem .settings-select {
  border-color: var(--color-danger);
}

.integration-port-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.integration-port-row .settings-select {
  flex: 1 1 auto;
  min-width: 0;
}

/* Re-scan is one word and must stay one line: wrapped, it grew taller than
   the select beside it and took the width the device path needed. It is the
   select's height, too, so the Port row is no taller than the rows around it. */
.integration-port-row .settings-action {
  flex: none;
  white-space: nowrap;
  height: var(--control-height);
  padding-block: 0;
}

/* "Advanced" is a row of its own, in the rows' type — a disclosure, so in the
   secondary colour until pointed at. */
.integration-advanced > summary {
  padding: var(--space-3) 0;
  font-size: var(--font-size);
  color: var(--color-subtext);
  cursor: pointer;
}

.integration-advanced > summary:hover {
  color: var(--color-text);
}

/* The Mock's one line, and a note under the buttons: `.settings-hint`s, with
   a note coloured by what it says. */
.integration-mock-note {
  margin-bottom: var(--space-3);
}

.integration-note--warn {
  color: var(--color-warning);
}

.integration-note--ok {
  color: var(--color-success);
}

.settings-action--primary {
  border-color: var(--color-accent);
}

.settings-action:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ── The Properties card's Pins editor (pin-fields-editor.js) ────────────── */
.pin-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.pin-fields-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.pin-fields-pins {
  flex: 0 0 3.5em;
  color: var(--color-muted);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  font-variant-numeric: tabular-nums;
}

.pin-fields-name {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--control-height);
  box-sizing: border-box;
  padding: 0 var(--space-2);
  background: var(--color-surface-0);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

.pin-fields-type {
  flex: 0 0 auto;
  height: var(--control-height);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-sm);
}

.pin-fields-remove {
  flex: 0 0 auto;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  background: transparent;
  color: var(--color-muted);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--font-size);
  cursor: pointer;
}

.pin-fields-remove:enabled:hover {
  color: var(--color-danger);
  border-color: var(--color-overlay);
}

.pin-fields-remove:disabled {
  opacity: 0.3;
  cursor: default;
}

.pin-fields-add {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.pin-fields-count {
  flex: 1 1 auto;
  color: var(--color-muted);
  font-size: var(--font-size-xs);
}

.pin-fields-add-btn {
  padding: 2px var(--space-2);
  background: var(--color-surface-1);
  color: var(--color-text);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  cursor: pointer;
}

.pin-fields-add-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

/* ── Generate (codegen-dialog.js) ────────────────────────────────────────── */
.codegen-popup {
  width: 460px;
  max-width: 90vw;
}

.codegen-popup-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
}

.codegen-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.codegen-row {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
}

.codegen-row--stale,
.codegen-row--never {
  border-color: var(--color-warning);
}

.codegen-row--unknown {
  border-color: var(--color-danger);
}

.codegen-row-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}

.codegen-name {
  flex: 1 1 auto;
  font-weight: 600;
  color: var(--color-text);
}

.codegen-status {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.codegen-row--stale .codegen-status,
.codegen-row--never .codegen-status {
  color: var(--color-warning);
}

.codegen-row--current .codegen-status {
  color: var(--color-success);
}

.codegen-counts,
.codegen-language,
.codegen-hash {
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.codegen-hash {
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

.codegen-warnings {
  margin: 0;
  padding-left: var(--space-4);
  color: var(--color-warning);
  font-size: var(--font-size-sm);
}

.codegen-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

/* ── Generate ▸ View files (code-files-dialog.js) ────────────────────────── */
/* Wide and tall enough to read code in: the card is a frame around a
   listing that scrolls both ways, with the tabs above and Copy / Save As…
   below. Nothing on the card but a file's text can be selected
   (.code-files-code opts back in), so what is highlighted is exactly what
   Copy takes. */
.code-files-popup {
  display: flex;
  flex-direction: column;
  width: min(820px, 90vw);
  max-width: 90vw;
  height: min(640px, 85vh);
  overflow: hidden;
  user-select: none;
}

.code-files-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  padding: var(--space-3) var(--space-4) var(--space-4);
}

.code-files-panels {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  border: 1px solid var(--color-overlay);
  border-top: none;
  background: var(--color-mantle);
}

.code-files-panel {
  flex: 1 1 auto;
  min-width: 0;
  overflow: auto;
}

.code-files-panel[hidden] {
  display: none;
}

.code-files-panel:focus {
  outline: none;
}

.code-files-panel:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* The gutter and the text are two <pre>s side by side, so the text is one
   run a selection sweeps like any text view's. As wide as the longest line
   (the panel scrolls it), and at least as tall as the panel, so the gutter's
   rule runs to the bottom of a short file. */
.code-files-listing {
  display: flex;
  width: max-content;
  min-width: 100%;
  min-height: 100%;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-text);
}

.code-files-gutter,
.code-files-code {
  margin: 0;
  font: inherit;
  white-space: pre;
}

/* The line numbers: out of any selection, and pinned to the left edge so a
   long line scrolls under them rather than away from them. */
.code-files-gutter {
  flex: none;
  position: sticky;
  left: 0;
  padding: 0 var(--space-2) 0 var(--space-3);
  border-right: 1px solid var(--color-overlay);
  background: var(--color-mantle);
  color: var(--color-muted);
  text-align: right;
  user-select: none;
}

.code-files-code {
  flex: 1 1 auto;
  padding: 0 var(--space-3);
  user-select: text;
}

.code-files-footer {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-top: var(--space-3);
}

.code-files-result {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
  overflow-wrap: anywhere;
}

/* ── A connection window (serial-log.html) ──────────────────────────────── */
.serial-log-body {
  height: 100vh;
  margin: 0;
  background: var(--color-surface-0);
  color: var(--color-text);
  font-family: var(--font-sans);
  overflow: hidden;
}

.serial-log-root {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* Basis 0, not auto: the stream takes whatever the rows under it leave and
   scrolls inside it. With `auto` its basis was the height of EVERY line, so
   each new line squeezed the Mock's panel below (a shrinkable sibling takes
   its share of the overflow) until the panel scrolled away to a sliver. */
.serial-log-scroll {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  line-height: 1.45;
  user-select: text;
}

/* One line: [time] [prefix] text — the time column shown only with
   Timestamps on, the prefix one character wide so every kind lines up. */
.serial-log-line {
  display: flex;
  gap: var(--space-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.serial-log-time {
  display: none;
  flex: 0 0 auto;
  color: var(--color-muted);
  white-space: pre;
}

.serial-log--times .serial-log-time {
  display: inline;
}

.serial-log-prefix {
  flex: 0 0 1ch;
  white-space: pre;
}

.serial-log-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* The kinds, by colour: data in the lamps' own (→ is TX's, ← RX's),
   protocol dim, errors in the warning colour. */
.serial-log-line--out {
  color: var(--color-sim-high);
}

.serial-log-line--in {
  color: var(--color-info);
}

.serial-log-line--protocol {
  color: var(--color-muted);
}

.serial-log-line--error {
  color: var(--color-warning);
}

/* The filters: a view, never a rebuild. Errors have no filter. */
.serial-log--hide-log .serial-log-line--log,
.serial-log--hide-data .serial-log-line--data,
.serial-log--hide-protocol .serial-log-line--protocol {
  display: none;
}

.serial-log-partial {
  color: var(--color-subtext);
}

.serial-log-partial[hidden],
.serial-log-empty[hidden] {
  display: none;
}

.serial-log-empty {
  margin: var(--space-4) 0;
  font-family: var(--font-sans);
  color: var(--color-muted);
  text-align: center;
}

/* One thin row under the stream. */
.serial-log-footer {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-3);
  border-top: 1px solid var(--color-overlay);
  background: var(--color-mantle);
  font-size: var(--font-size-sm);
}

.serial-log-toggle input {
  accent-color: var(--color-accent);
}

.serial-log-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-subtext);
  white-space: nowrap;
  cursor: pointer;
}

.serial-log-gap {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-align: center;
}

.serial-log-note {
  color: var(--color-subtext);
  font-size: var(--font-size-xs);
  white-space: nowrap;
  text-overflow: ellipsis;
}

.serial-log-footer .settings-action {
  flex: 0 0 auto;
}

/* Every button in the window is one control tall, the height of the Mock's
   log field, so a button and the field beside it share both edges. */
.serial-log-root .settings-action {
  height: var(--control-height);
  padding-block: 0;
}

/* ── The built-in Mock's send panel, docked under its log ───────────────── */
.mock-panel {
  container: mock-panel / inline-size;
  flex: 0 1 auto;
  max-height: 55vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3) var(--space-3);
  border-top: 1px solid var(--color-overlay);
  background: var(--color-mantle);
  font-size: var(--font-size-sm);
}

.mock-status {
  margin: 0;
  color: var(--color-subtext);
}

.mock-status--connected {
  color: var(--color-success);
}

.mock-hint {
  margin: 0;
  color: var(--color-muted);
}

.mock-inputs {
  /* ONE grid for every Input, so a column lines up down the whole panel:
     1 the Input's name · 2 a field's name · 3 its bits · 4 its hex ·
     5 the slack · 6 the result · 7 Send. Each row and each field line is a
     subgrid of it. The slack is what holds every Send on the right edge,
     flush with Send all, Log and the footer's Save…. */
  display: grid;
  grid-template-columns:
    fit-content(10em) fit-content(8em) auto auto minmax(0, 1fr)
    minmax(0, auto) auto;
  column-gap: var(--space-2);
  row-gap: var(--space-2);
}

.mock-inputs > .mock-hint {
  grid-column: 1 / -1;
}

/* Everything in a row hangs from its FIRST line, and a line is one control
   tall, so the name, the first field and Send share one centre line however
   many fields the Input has. A rule under EVERY Input, the last included,
   rules the rows off from the Faults line below as well as from each other. */
.mock-input {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: start;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-overlay);
}

/* The name, behind the caret that folds its row. It gives way to a narrow
   window, down to a floor — below it the panel scrolls sideways rather than
   lose it altogether. */
.mock-input-toggle {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 4em;
  height: var(--control-height);
  padding: 0 var(--space-2) 0 0;
  border: none;
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.mock-input-toggle:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.mock-input-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The caret points down when open, right when folded. */
.mock-input-toggle[aria-expanded="false"] .mock-caret,
.mock-faults:not([open]) .mock-caret {
  transform: rotate(-90deg);
}

.mock-fields {
  grid-column: 2 / span 3;
  grid-row: 1;
  display: grid;
  grid-template-columns: subgrid;
}

/* The class's `display` outranks the UA sheet's [hidden]. */
.mock-fields[hidden] {
  display: none;
}

/* One line per field: name · bits · hex, each centred on the line's FIRST
   control-high band — the band the Input's name and Send sit on — so a
   word's second byte hangs below without moving anything beside it. */
.mock-field {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: start;
  --mock-inset: calc((var(--control-height) - var(--segment-height)) / 2);
}

.mock-field-name {
  min-width: 3em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-subtext);
  line-height: var(--control-height);
}

/* A byte to a row, MSB first — a word stacks its high byte over its low one,
   so the bits column is always one byte wide and the hex fields line up. */
.mock-bits {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding-block: var(--mock-inset);
}

.mock-byte {
  display: inline-flex;
  gap: 2px;
}

/* One glyph in a box, which is what a segment is sized for. */
.mock-bit {
  min-width: var(--segment-height);
  height: var(--segment-height);
  padding: 0 var(--space-1);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-0);
  color: var(--color-subtext);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
  cursor: pointer;
}

/* A gap every four bits, so a byte reads as two hex digits. */
.mock-bit--nibble {
  margin-left: var(--space-1);
}

.mock-bit[aria-pressed="true"] {
  border-color: var(--color-sim-high);
  background: var(--color-sim-high);
  color: var(--color-base);
}

.mock-bit:disabled {
  cursor: default;
  opacity: 0.5;
}

.mock-hex {
  width: 5.5em;
  height: var(--segment-height);
  margin-block: var(--mock-inset);
  padding: 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

/* Gives way before anything else does: a long reason is cut, whole in its
   tooltip. */
.mock-result {
  grid-column: 6;
  grid-row: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  color: var(--color-success);
  font-size: var(--font-size-xs);
  line-height: var(--control-height);
}

.mock-result--failed {
  color: var(--color-warning);
}

.mock-send {
  grid-column: 7;
  grid-row: 1;
}

/* Too narrow for one shared row: each name heads its Input, beside the
   result and Send, and the fields go under it on a grid of their own — its
   name column a fixed width, so bits and hex fields still line up down the
   panel — and Send never scrolls out of sight. Container ems are the panel's
   own text, so the break moves with the font size. */
@container mock-panel (max-width: 43em) {
  .mock-inputs {
    display: flex;
    flex-direction: column;
  }

  .mock-input {
    display: flex;
    flex-wrap: wrap;
    align-items: start;
    column-gap: var(--space-2);
  }

  .mock-input-toggle {
    flex: 1 1 0;
  }

  /* Indented past the caret (8px wide, then its gap), under the name. */
  .mock-fields {
    order: 1;
    flex: 1 0 100%;
    padding-left: calc(8px + var(--space-1));
    grid-template-columns: 4em max-content max-content;
    justify-content: start;
    column-gap: var(--space-2);
  }
}

/* Faults and Send all on one line. The disclosure spans the whole line, so
   when it opens its hint and buttons get the panel's full width below; Send
   all sits over the right end of the summary line, which is one control
   tall so the two share a centre line. */
.mock-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
}

.mock-actions > .mock-faults {
  grid-column: 1 / -1;
  grid-row: 1;
}

.mock-send-all {
  grid-column: 2;
  grid-row: 1;
}

/* Ruled off from Faults and Send all above as one Input is from the next.
   The rule is the log row's own, so it goes when the Log filter hides it. */
.mock-log-row {
  display: flex;
  gap: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-overlay);
}

/* The class's `display` outranks the UA sheet's [hidden]. */
.mock-log-row[hidden] {
  display: none;
}

.mock-log-input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
}

.mock-faults > summary {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  height: var(--control-height);
  cursor: pointer;
  color: var(--color-subtext);
}

.mock-faults > summary::-webkit-details-marker {
  display: none;
}

.mock-faults[open] > summary {
  margin-bottom: var(--space-2);
}

.mock-fault-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.mock-fault--armed {
  border-color: var(--color-warning);
  color: var(--color-warning);
}

/* ── Export To report (export-report-dialog.js, Feature 390) ─────────────── */
.export-report-popup {
  width: 480px;
  max-width: 90vw;
}

.export-report-group + .export-report-group {
  margin-top: var(--space-3);
}

.export-report-heading {
  margin: 0 0 var(--space-1);
  font-size: var(--font-size);
  font-weight: 600;
}

.export-report-heading--dropped {
  color: var(--color-danger);
}

.export-report-heading--changed,
.export-report-heading--footprint {
  color: var(--color-warning);
}

.export-report-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
}

/* Designators · part · why — one aligned row per part (or run of parts). */
.export-report-row {
  display: grid;
  grid-template-columns: minmax(3.5em, auto) minmax(5em, auto) 1fr;
  gap: var(--space-2);
  color: var(--color-subtext);
}

/* A line about a whole kind of thing (the external signals), not a part. */
.export-report-row--whole {
  display: block;
}

.export-report-refs {
  font-family: var(--font-mono);
  color: var(--color-text);
}

.export-report-part {
  color: var(--color-text);
}

.export-report-note {
  margin: var(--space-3) 0 0;
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.export-report-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* ── The chip designer window (custom chips) ──────────────────────────────
   web/chip-designer.html — one window: the DESIGNER while stopped (a custom
   chip's package and Verilog), the DEBUGGER while running. The package on
   the left, the code on the right, tabs across the top. Its tabs and its
   debugger bar are the CPU monitor's too (`cpumon-tabs`, `cpumon-bar`), so
   the two debugging windows read as one family. */
.cd-root {
  display: flex;
  flex-direction: column;
  height: 100vh;
  min-height: 0;
}

.cd-tabs,
.cpumon-tabs {
  display: flex;
  align-items: flex-end;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3) 0;
  background: var(--color-mantle);
  border-bottom: 1px solid var(--color-overlay);
  overflow-x: auto;
  flex: 0 0 auto;
}

.cd-tab,
.cpumon-tab {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-overlay);
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: var(--color-surface-0);
  color: var(--color-subtext);
  white-space: nowrap;
}

.cd-tab--active,
.cpumon-tab--active {
  background: var(--color-base);
  color: var(--color-text);
}

.cd-tab-label,
.cpumon-tab-label {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: var(--segment-height);
  padding: 0 var(--space-2) 0 var(--space-3);
  border: none;
  background: transparent;
  color: inherit;
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.cd-tab-badge,
.cpumon-tab-badge {
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  font-size: var(--font-size-xs);
  background: var(--color-surface-1);
  color: var(--color-subtext);
}

.cd-tab-badge--paused,
.cpumon-tab-badge--paused {
  background: var(--color-debug-pause);
  color: var(--color-crust);
}

.cd-tab-badge--settled {
  background: var(--color-success);
  color: var(--color-crust);
}

.cd-tab-badge--detached {
  background: transparent;
  border: 1px dashed var(--color-overlay);
}

.cd-tab-close,
.cd-tab-new,
.cd-icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--segment-height);
  height: var(--segment-height);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font-size: var(--font-size);
  cursor: pointer;
}

.cd-tab-close:hover,
.cd-tab-new:hover,
.cd-icon-button:enabled:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

.cd-tab-new {
  margin-bottom: 2px;
}

/* The debugger's own bar: the toolbar's pill shape, scoped to one chip. */
.cd-bar,
.cpumon-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-overlay);
  background: var(--color-mantle);
  flex: 0 0 auto;
}

.cd-bar[hidden] {
  display: none;
}

.cd-bar-label {
  padding: 0 var(--space-1) 0 var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-muted);
}

.cd-bar-lamp {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.cd-lamp {
  width: 0.8em;
  height: 0.8em;
  border-radius: 50%;
  background: var(--color-sim-float);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}

.cd-lamp--settled {
  background: var(--color-sim-high);
}

.cd-lamp--busy {
  background: var(--color-debug-pause);
}

.cd-bar-status,
.cpumon-bar-status {
  flex: 1 1 auto;
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.cd-notice {
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-1);
  border-bottom: 1px solid var(--color-overlay);
  font-size: var(--font-size-sm);
  color: var(--color-warning);
}

.cd-notice[hidden],
.cd-main[hidden],
.cd-empty[hidden],
.cd-watch[hidden],
.cd-field-error[hidden],
.cd-watch-unit[hidden] {
  display: none;
}

.cd-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  flex: 1 1 auto;
  padding: var(--space-5);
  text-align: center;
  color: var(--color-muted);
}

/* The package on the left, the code on the right, and the divider between
   them (`.cd-split`), which the user drags: `--cd-left-width` is where it was
   left (chip-designer-view.js), clamped here to what the window has room for
   — the same 300 px and 360 px floors the drag itself keeps. */
.cd-main {
  display: grid;
  grid-template-columns:
    clamp(300px, var(--cd-left-width, 420px), calc(100% - 360px))
    minmax(0, 1fr);
  flex: 1 1 auto;
  min-height: 0;
}

/* Laid over the border between the two, straddling it, so it takes no
   width from either — the parts tray's resize seam stood in a grid. */
.cd-split {
  position: relative;
  z-index: 4;
  grid-column: 2;
  grid-row: 1;
  justify-self: start;
  width: 6px;
  margin-left: -3px;
  cursor: ew-resize;
  touch-action: none;
}

.cd-split::after,
.cd-code-split::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 3px;
  height: 44px;
  transform: translate(-50%, -50%);
  border-radius: var(--radius-sm);
  background: var(--color-overlay);
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
}

.cd-code-split::after {
  width: 44px;
  height: 3px;
}

.cd-split:hover::after,
.cd-split--active::after,
.cd-code-split:hover::after,
.cd-code-split--active::after {
  opacity: 1;
}

/* Every pane the designer redraws wholesale (each edit, each echo, each
   debugger tick) opts out of scroll anchoring: the browser's anchor is a node
   the redraw has just replaced, and re-anchoring on its successor threw the
   package pane hundreds of pixels — usually back to the top — on every edit. */
.cd-tabs,
.cd-package,
.cd-form-pane,
.cd-problems,
.cd-watch-scroll {
  overflow-anchor: none;
}

/* The package drawing stays at the top, always in view, and only the form
   under the rule scrolls. The form's basis is 0, so it takes whatever the
   drawing leaves: a package too tall to leave the form its floor (a 40-pin
   part in a short window) shrinks and scrolls itself instead. Both halves
   are placed by hand: the divider shares the right one's cell, and an
   auto-placed half would step round it onto a row of its own. */
.cd-left {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  border-right: 1px solid var(--color-overlay);
  background: var(--color-surface-0);
}

.cd-form-pane {
  flex: 1 1 0;
  min-height: 12em;
  overflow: auto;
  padding: var(--space-2) var(--space-3) var(--space-3);
}

.cd-right {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: var(--space-3);
  gap: var(--space-3);
}

/* ── The package, as a datasheet draws it ─────────────────────────────── */
.cd-package {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  flex: 0 1 auto;
  min-height: 0;
  overflow: auto;
  padding: var(--space-4) var(--space-3) var(--space-3);
}

.cd-package-grid {
  display: grid;
  grid-template-columns: minmax(0, auto) auto 6.5em auto minmax(0, auto);
  row-gap: 2px;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

.cd-package-body {
  position: relative;
  grid-column: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 3em;
  border-radius: 4px;
  background: var(--color-chip-custom-body);
  color: var(--color-chip-label);
  overflow: hidden;
}

.cd-package-body--wide {
  min-width: 9em;
}

.cd-package-notch {
  position: absolute;
  top: -0.6em;
  left: 50%;
  width: 1.2em;
  height: 1.2em;
  transform: translateX(-50%);
  border-radius: 50%;
  background: var(--color-surface-0);
}

/* The folded corner, as on the desk. */
.cd-package-fold {
  position: absolute;
  top: 0;
  right: 0;
  width: 1.4em;
  height: 1.4em;
  background: linear-gradient(
    225deg,
    var(--color-surface-0) 0 50%,
    var(--color-chip-custom-fold) 50% 100%
  );
}

.cd-package-glyph {
  display: inline-flex;
  color: var(--color-chip-custom-glyph);
}

.cd-package-marking {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-weight: 600;
  letter-spacing: 0.08em;
}

.cd-pin {
  display: flex;
  align-items: center;
  min-height: 1.8em;
  cursor: default;
}

.cd-pin-number {
  justify-content: center;
  min-width: 2.2em;
  padding: 0 var(--space-1);
  background: var(--color-chip-leg);
  color: var(--color-crust);
  font-weight: 600;
}

.cd-pin--left.cd-pin-number {
  border-radius: 3px 0 0 3px;
}

.cd-pin--right.cd-pin-number {
  border-radius: 0 3px 3px 0;
}

.cd-pin-name {
  max-width: 10em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 0 var(--space-2);
}

.cd-pin--left.cd-pin-name {
  justify-content: flex-end;
}

.cd-pin--input.cd-pin-name {
  color: var(--color-code-signal);
}

.cd-pin--output.cd-pin-name {
  color: var(--color-code-pin);
}

/* An inout pin is both: the pin colour, and italic so it is told apart from
   an output by more than a hue. */
.cd-pin--io.cd-pin-name {
  color: var(--color-code-pin);
  font-style: italic;
}

.cd-pin--vcc.cd-pin-name {
  color: var(--color-rail-plus);
}

.cd-pin--gnd.cd-pin-name {
  color: var(--color-rail-minus);
}

.cd-pin--nc.cd-pin-name {
  color: var(--color-muted);
}

.cd-pin--level-h.cd-pin-number {
  background: var(--color-sim-high);
}

.cd-pin--level-l.cd-pin-number {
  background: var(--color-sim-low);
  color: var(--color-text);
}

.cd-pin--level-z.cd-pin-number {
  background: var(--color-sim-float);
}

.cd-pin--level-x.cd-pin-number {
  background: var(--color-sim-conflict);
}

.cd-pin--linked {
  outline: 2px solid var(--color-code-pin);
  outline-offset: -1px;
  background-color: var(--color-code-linked);
}

/* ── The package form ─────────────────────────────────────────────────── */
.cd-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.cd-row {
  /* The containing block for this row's own note card (see .cd-note). */
  position: relative;
  display: grid;
  grid-template-columns: 8em minmax(0, 1fr);
  align-items: center;
  column-gap: var(--space-3);
  row-gap: var(--space-1);
  padding: var(--space-1) 0;
}

.cd-label {
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

.cd-row-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-width: 0;
}

/* A row's note, opened by the (i) at the end of its controls (info-button.js).
   It FLOATS over the rows below — under the controls it explains, the grid
   area being its containing block — rather than pushing them down, which
   would move the very control the reader is about to reach for. As Settings'
   .settings-note. */
.cd-note {
  position: absolute;
  grid-column: 2;
  grid-row: 1;
  top: calc(100% + var(--space-1));
  left: 0;
  right: 0;
  z-index: 3;
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface-1);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-1);
}

.cd-note-text {
  margin: 0;
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-subtext);
}

.cd-input,
.cd-select {
  height: var(--control-height);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
}

.cd-input {
  flex: 1 1 8em;
  min-width: 0;
}

.cd-input--number {
  flex: 0 0 5em;
}

.cd-input--port {
  font-family: var(--font-mono);
}

.cd-input[aria-invalid="true"] {
  border-color: var(--color-danger);
}

.cd-input:focus-visible,
.cd-select:focus-visible,
.cd-button:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.cd-field-error {
  width: 100%;
  font-size: var(--font-size-xs);
  color: var(--color-danger);
}

.cd-hint {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* The line between the package drawing and the form scrolling under it. */
.cd-rule {
  flex: 0 0 auto;
  margin: 0 var(--space-3);
  border: none;
  border-top: 1px solid var(--color-overlay);
}

.cd-subhead {
  margin: var(--space-3) 0 var(--space-1);
  font-size: var(--font-size-sm);
  font-weight: 700;
  color: var(--color-subtext);
}

.cd-subhead-row,
.cd-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.cd-button {
  height: var(--segment-height);
  padding: 0 var(--space-3);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-1);
  color: var(--color-text);
  font-size: var(--font-size-sm);
  cursor: pointer;
}

.cd-button:enabled:hover {
  background: var(--color-surface-0);
}

.cd-button:disabled,
.cd-input:disabled,
.cd-select:disabled {
  opacity: 0.5;
  cursor: default;
}

/* The ports table, each port's pins on its own row (chip-package-form.js).
   The pin columns grow with the units, so the table scrolls SIDEWAYS on its
   own when the pane is too narrow for them: the port (name, direction,
   width) is held at the left and its remove button at the right — sticky,
   on the pane's own background so the pins slide UNDER them — and every row
   is one grid of the same columns, so they line up. The port column takes
   whatever is left over; below its floor the table overflows and scrolls. */
.cd-ports {
  overflow-x: auto;
  overflow-y: hidden;
  /* Room for a focused control's outline, which the scroller would clip. */
  padding: 2px 0;
}

.cd-port-table {
  --cd-port-min: 15.5em;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: min-content;
}

.cd-port-row {
  display: grid;
  grid-template-columns:
    minmax(var(--cd-port-min), 1fr) repeat(var(--cd-units, 1), 4.2em)
    var(--segment-height);
  align-items: center;
  gap: var(--space-1);
}

.cd-port-row--head {
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.cd-port-cell {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  min-width: 0;
  align-self: stretch;
  background: var(--color-surface-0);
}

.cd-port-cell--port {
  position: sticky;
  left: 0;
  z-index: 1;
}

.cd-port-cell--remove {
  position: sticky;
  right: 0;
  z-index: 1;
}

/* Name, direction and width — and their headings above them — on the same
   bases, so a heading stands over its control. */
.cd-port-cell--port > .cd-input--port,
.cd-port-head-name {
  flex: 1 1 4.5em;
  min-width: 4.5em;
}

.cd-port-cell--port > .cd-select:first-of-type,
.cd-port-head-dir {
  flex: 0 0 6.5em;
}

.cd-port-cell--port > .cd-select:last-of-type,
.cd-port-head-width {
  flex: 0 0 4em;
}

/* A bus's bits, under its row: the bit's name where the port's name is. */
.cd-port-row--bit .cd-port-cell--port {
  padding-left: var(--space-3);
}

.cd-port-unit {
  white-space: nowrap;
}

.cd-map-port {
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
}

.cd-map-port--input {
  color: var(--color-code-signal);
}

.cd-map-port--output {
  color: var(--color-code-pin);
}

.cd-map-port--inout {
  color: var(--color-code-pin);
  font-style: italic;
}

/* ── The code ─────────────────────────────────────────────────────────── */
.cd-code {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 12em;
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-crust);
  overflow: hidden;
}

.cd-code-fixed {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  line-height: 1.5;
  color: var(--color-muted);
  background: var(--color-mantle);
  white-space: pre;
  overflow-x: auto;
  user-select: text;
}

/* The generated header is as tall as its text needs, up to two fifths of
   the code box — the body under it is what is being written — unless the
   user has dragged the divider under it (chip-designer-view.js sets its
   basis and lifts the cap). Either way it is the one that gives way when the
   code is short of room: the body below starts from nothing and keeps its
   floor, so the header shrinks first, down to two lines, and scrolls. */
.cd-code-header {
  flex: 0 1 auto;
  min-height: calc(3em + 2 * var(--space-2));
  max-height: 40%;
  overflow-y: auto;
  border-bottom: 1px dashed var(--color-overlay);
}

.cd-code-footer {
  flex: 0 0 auto;
  border-top: 1px dashed var(--color-overlay);
}

/* The divider under the header: no height of its own, a band either side of
   the dashed line to grab, drawn over the code below it. */
.cd-code-split {
  position: relative;
  z-index: 2;
  flex: 0 0 0;
  touch-action: none;
}

.cd-code-split::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -3px;
  height: 6px;
  cursor: ns-resize;
}

.hdl-editor {
  display: flex;
  flex: 1 1 0;
  min-height: 8em;
  font-family: var(--font-mono);
  font-size: var(--font-size-sm);
  line-height: 1.5;
}

.hdl-editor-gutter {
  flex: 0 0 auto;
  min-width: 3em;
  padding: var(--space-2) var(--space-2) var(--space-2) 0;
  overflow: hidden;
  text-align: right;
  color: var(--color-muted);
  background: var(--color-mantle);
  user-select: none;
}

.hdl-editor-lineno--error {
  color: var(--color-code-bad);
  font-weight: 700;
}

.hdl-editor-lineno--warn {
  color: var(--color-warning);
}

.hdl-editor-lineno--current {
  color: var(--color-debug-pause);
  font-weight: 700;
}

/* A line number is where a breakpoint goes (hdl-editor.js): click it. */
.hdl-editor-lineno {
  position: relative;
  cursor: pointer;
}

.hdl-editor-lineno:hover {
  color: var(--color-text);
}

/* A breakpoint is a red circle where its line's number was — solid where
   the code can stop, hollow on a line it never can (kept, and ignored). The
   number stays in the row, unseen, so the gutter keeps its width. */
.hdl-editor-lineno.hdl-editor-lineno--break,
.hdl-editor-lineno.hdl-editor-lineno--break-idle {
  color: transparent;
}

.hdl-editor-lineno--break::after,
.hdl-editor-lineno--break-idle::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 0;
  box-sizing: border-box;
  width: 0.85em;
  height: 0.85em;
  border-radius: 50%;
  transform: translateY(-50%);
}

.hdl-editor-lineno--break::after {
  background: var(--color-danger);
}

.hdl-editor-lineno--break-idle::after {
  border: 2px solid var(--color-danger);
}

.hdl-editor-stack {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
}

/* The coloured text and the textarea over it share every metric, or the
   caret drifts from the letters it sits between. */
.hdl-editor-code,
.hdl-editor-input {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  line-height: inherit;
  white-space: pre;
  tab-size: 2;
  letter-spacing: 0;
}

.hdl-editor-code {
  color: var(--color-text);
  pointer-events: none;
}

.hdl-editor-input {
  width: 100%;
  height: 100%;
  border: none;
  outline: none;
  resize: none;
  overflow: auto;
  background: transparent;
  color: transparent;
  caret-color: var(--color-text);
}

.hdl-editor-input::selection {
  background: color-mix(in srgb, var(--color-info) 40%, transparent);
  color: transparent;
}

.hdl-editor--readonly .hdl-editor-input {
  caret-color: transparent;
}

/* The line the debugger is paused on. */
.hdl-editor-line {
  position: absolute;
  left: 0;
  right: 0;
  background: var(--color-code-current);
  border-left: 3px solid var(--color-debug-pause);
  pointer-events: none;
}

.hdl-editor-line[hidden] {
  display: none;
}

.hdl-tok--keyword {
  color: var(--color-code-keyword);
}

.hdl-tok--number {
  color: var(--color-code-number);
}

.hdl-tok--comment {
  color: var(--color-code-comment);
  font-style: italic;
}

.hdl-tok--operator {
  color: var(--color-code-operator);
}

.hdl-tok--pin {
  color: var(--color-code-pin);
  font-weight: 600;
}

.hdl-tok--signal {
  color: var(--color-code-signal);
}

.hdl-tok--bad,
.hdl-tok--string {
  color: var(--color-code-bad);
}

.hdl-tok--error {
  text-decoration: wavy underline var(--color-code-bad);
  text-decoration-skip-ink: none;
}

.hdl-tok--warn {
  text-decoration: wavy underline var(--color-warning);
  text-decoration-skip-ink: none;
}

.hdl-tok--current {
  background: var(--color-code-current);
  border-radius: 2px;
}

.hdl-tok--linked {
  background: var(--color-code-linked);
  border-radius: 2px;
  outline: 1px solid var(--color-code-pin);
}

/* ── Problems and the watch panel ─────────────────────────────────────── */
.cd-problems-section {
  flex: 0 0 auto;
}

.cd-problems {
  max-height: 9em;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
  font-size: var(--font-size-sm);
}

.cd-problem {
  display: flex;
  gap: var(--space-2);
  padding: 2px var(--space-2);
  border-left: 3px solid transparent;
  cursor: default;
}

.cd-problem[tabindex="0"] {
  cursor: pointer;
}

.cd-problem[tabindex="0"]:hover {
  background: var(--color-surface-0);
}

.cd-problem--error {
  border-left-color: var(--color-code-bad);
}

.cd-problem--warning {
  border-left-color: var(--color-warning);
}

.cd-problem--none {
  color: var(--color-muted);
}

.cd-problem-where {
  flex: 0 0 auto;
  min-width: 5em;
  color: var(--color-muted);
}

.cd-watch {
  display: flex;
  flex-direction: column;
  flex: 0 1 auto;
  min-height: 6em;
  max-height: 40%;
}

.cd-watch-scroll {
  overflow: auto;
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
}

.cd-watch-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--font-size-sm);
}

.cd-watch-table th {
  position: sticky;
  top: 0;
  padding: 2px var(--space-2);
  text-align: left;
  font-weight: 600;
  color: var(--color-muted);
  background: var(--color-mantle);
}

.cd-watch-table td {
  padding: 2px var(--space-2);
  border-top: 1px solid var(--color-surface-0);
}

.cd-watch-name,
.cd-watch-value {
  font-family: var(--font-mono);
}

.cd-watch-row--input .cd-watch-name {
  color: var(--color-code-signal);
}

.cd-watch-row--output .cd-watch-name,
.cd-watch-row--inout .cd-watch-name {
  color: var(--color-code-pin);
}

.cd-watch-row--inout .cd-watch-name {
  font-style: italic;
}

.cd-watch-drives {
  color: var(--color-muted);
}

.cd-watch-toggle {
  padding: 0 var(--space-2);
  border: 1px solid var(--color-overlay);
  border-radius: var(--radius-sm);
  background: var(--color-surface-0);
  color: var(--color-text);
  font: inherit;
  font-family: var(--font-sans);
  cursor: pointer;
}

.cd-watch-toggle:hover {
  background: var(--color-surface-1);
}

/* An array's words: a virtualized grid (hdl-memory-view.js) in a row of its
   own under the array's, tall enough for a dozen rows and scrolling past
   that. The rows are placed absolutely by the view, which sets their top
   and height. */
.cd-watch-memory-row td {
  padding: 0 0 var(--space-1) var(--space-4);
}

.cd-memory-scroll {
  position: relative;
  max-height: 14em;
  overflow: auto;
  overflow-anchor: none;
  border: 1px solid var(--color-surface-0);
  border-radius: var(--radius-sm);
}

.cd-memory-canvas {
  position: relative;
}

.cd-memory-row {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-2);
  font-family: var(--font-mono);
  white-space: nowrap;
}

.cd-memory-row[hidden],
.cd-memory-word[hidden] {
  display: none;
}

.cd-memory-index {
  min-width: 4ch;
  color: var(--color-muted);
}

.cd-memory-word--pending {
  color: var(--color-debug-pause);
  font-weight: 600;
}

.cd-watch-kind,
.cd-watch-decimal,
.cd-watch-empty {
  color: var(--color-muted);
}

.cd-watch-pending {
  color: var(--color-debug-pause);
  font-weight: 600;
}

/* ── Settings ▸ Spice Lite (settings-spice-panel.js) ───────────────────────
   Label left, number right: every row (the Gap and each Advanced field) ends
   in the same group — one fixed-width field, one fixed-width unit — so the
   fields are one width and one column down the panel. Unlike the other tabs'
   field rows (label a quarter of the row, field filling the rest), the label
   here takes the slack, since a number wants a field sized for a number. */
.spice-field .settings-label {
  flex: 1;
  min-width: 0;
}

.spice-number-group {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--space-2);
}

.settings-text-input.spice-number {
  flex: 0 0 auto;
  width: 9em;
  text-align: right;
}

.spice-number[aria-invalid="true"] {
  border-color: var(--color-danger);
}

/* Why a field's value was refused (its range, or VIL under VIH), under the
   field it belongs to — right-aligned to sit beneath the number. */
.spice-problem {
  margin: 0 0 var(--space-2);
  text-align: right;
  color: var(--color-danger);
}

.spice-unit {
  flex: none;
  width: 2em;
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
}

/* The TTL | CMOS strip heads the family settings. */
.spice-family-strip {
  display: flex;
  padding: var(--space-3) 0 var(--space-1);
}

/* The family's source line beside its Reset button. */
.spice-family-head .spice-family-source {
  flex: 1;
  margin: 0;
}

/* "Advanced" is a disclosure row in the rows' type — Serial I/O's shape. */
.spice-advanced > summary {
  padding: var(--space-3) 0;
  font-size: var(--font-size);
  color: var(--color-subtext);
  cursor: pointer;
}

.spice-advanced > summary:hover {
  color: var(--color-text);
}

/* A PSU's live current under Spice Lite (psu-view.js `setSupply`): under
   the voltage badge at the clock brick's rate size (.part-clock-badge), amber while the supply is at its current limit
   and drooping. Empty text the rest of the time, so nothing shows. */
.part-psu-readout {
  fill: var(--color-psu-badge);
  font-family: var(--font-mono);
  font-size: 0.99px; /* SVG user units = pitch units */
  opacity: 0.85;
  user-select: none;
}

.part-psu--limited .part-psu-readout {
  fill: var(--color-psu-limited);
  opacity: 1;
}

/* ── The CPU monitor window ──────────────────────────────────────────────
   web/cpu-monitor.html — a live view of one CPU on the desk
   (components/cpu-monitor-view.js): memory on the left; flags, pins, step
   and clock over the instruction pipeline; the registers down the whole of
   the next column (a Z80 has seventeen); the stack, the breakpoints and the
   Z80's I/O ports stacked down the last. */
.cpumon-root {
  display: flex;
  flex-direction: column;
  height: 100vh;
  min-height: 0;
}

/* With no × beside it, a tab's label keeps the even padding. */
.cpumon-tab-label {
  padding-right: var(--space-3);
}

/* "Keep ROM edits after Stop" — the bar's one control besides the transport. */
.cpumon-keep {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--font-size-sm);
  color: var(--color-subtext);
  cursor: pointer;
}

.cpumon-keep[hidden] {
  display: none;
}

.cpumon-count {
  display: inline-flex;
  gap: var(--space-2);
  font-family: var(--font-mono);
}

.cpumon-count-value {
  color: var(--color-text);
  font-weight: 600;
}

.cpumon-body {
  display: grid;
  grid-template-columns: max-content max-content max-content minmax(
      max-content,
      1fr
    );
  grid-template-areas:
    "memory status registers side"
    "memory pipeline registers side";
  align-content: start;
  gap: var(--space-3);
  padding: var(--space-3);
  overflow: auto;
  flex: 1 1 auto;
  min-height: 0;
  font-family: var(--font-mono);
}

/* Stopped: the run's last moment, kept but plainly no longer live. */
.cpumon-root--stale .cpumon-body {
  opacity: 0.6;
}

.cpumon-empty {
  grid-column: 1 / -1;
  margin: var(--space-4) 0;
  font-family: var(--font-sans);
  color: var(--color-subtext);
}

.cpumon-empty[hidden],
.cpumon-panel[hidden] {
  display: none;
}

/* Nothing to show yet: the reason, then the breakpoints (set before Run). */
.cpumon-body--empty {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.cpumon-body--empty .cpumon-empty {
  margin-bottom: 0;
}

.cpumon-panel {
  min-width: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--color-mantle);
  border: 1px solid var(--color-surface-1);
  border-radius: var(--radius-sm);
}

.cpumon-panel-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-2);
}

.cpumon-panel-head .cpumon-title {
  flex: 1 1 auto;
  margin: 0;
}

.cpumon-title {
  margin: 0 0 var(--space-2);
  font-family: var(--font-sans);
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-warning);
}

.cpumon-memory {
  grid-area: memory;
}

.cpumon-status {
  grid-area: status;
}

.cpumon-pipeline {
  grid-area: pipeline;
}

.cpumon-registers {
  grid-area: registers;
}

.cpumon-side {
  grid-area: side;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
  /* As tall as the rows the other columns make, never taller: a height of 0
     adds nothing to the rows' size, and the minimum stretches it to them. */
  height: 0;
  min-height: 100%;
}

/* The window is as big as it gets, so the column shares its height out: the
   ports take what they need, and the Stack and Breakpoints split the rest
   evenly, each scrolling what does not fit. */
.cpumon-stack,
.cpumon-breaks {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-height: 0;
}

.cpumon-stack .cpumon-panel-body,
.cpumon-breaks .cpumon-panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.cpumon-ports {
  flex: 0 0 auto;
}

/* Nothing to show yet: the Breakpoints alone, at their own size. */
.cpumon-body--empty .cpumon-side {
  height: auto;
  min-height: 0;
}

.cpumon-body--empty .cpumon-breaks {
  flex: 0 0 auto;
}

/* Memory's Follow PC box and Go-to field. */
.cpumon-follow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  color: var(--color-subtext);
  cursor: pointer;
}

.cpumon-field {
  width: 9ch;
  padding: 1px var(--space-1);
  border: 1px solid var(--color-surface-1);
  border-radius: var(--radius-sm);
  background: var(--color-base);
  color: var(--color-text);
  font-family: var(--font-mono);
  font-size: var(--font-size-xs);
}

.cpumon-field:focus {
  outline: none;
  border-color: var(--color-accent);
}

.cpumon-field--invalid,
.cpumon-field--invalid:focus {
  border-color: var(--color-debug-break);
}

.cpumon-none {
  margin: 0;
  max-width: 24ch;
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.cpumon-label {
  font-family: var(--font-sans);
  color: var(--color-muted);
}

.cpumon-grid {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: center;
  gap: var(--space-1) var(--space-3);
}

/* Memory: sixteen rows of sixteen bytes, a gap after the eighth. */
.cpumon-mem-row {
  display: flex;
  gap: 1ch;
  line-height: 1.5;
}

.cpumon-mem-head {
  color: var(--color-muted);
}

.cpumon-mem-addr {
  width: 4ch;
  color: var(--color-muted);
}

.cpumon-byte {
  width: 2ch;
  text-align: center;
  color: var(--color-info);
}

.cpumon-mem-head .cpumon-byte {
  color: var(--color-muted);
}

.cpumon-byte--gap {
  margin-left: 1ch;
}

.cpumon-byte--unknown {
  color: var(--color-muted);
}

.cpumon-byte--op {
  color: var(--color-success);
}

.cpumon-byte--pc {
  color: var(--color-crust);
  background: var(--color-success);
  border-radius: var(--radius-sm);
}

.cpumon-byte--access {
  outline: 1px solid var(--color-warning);
  border-radius: var(--radius-sm);
}

.cpumon-byte--write {
  outline: 1px solid var(--color-warning);
  color: var(--color-warning);
  border-radius: var(--radius-sm);
}

/* A byte with a breakpoint is red (drawn after the PC's fill, so it wins). */
.cpumon-byte--break {
  color: var(--color-crust);
  background: var(--color-debug-break);
  border-radius: var(--radius-sm);
}

.cpumon-byte--selected {
  outline: 2px solid var(--color-text);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}

.cpumon-memory .cpumon-mem-row:not(.cpumon-mem-head) .cpumon-byte {
  cursor: pointer;
}

.cpumon-hint {
  margin: var(--space-2) 0 0;
  max-width: 52ch;
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

/* Flags, steps and pins. */
.cpumon-flags,
.cpumon-steps,
.cpumon-pins {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-1) var(--space-2);
}

.cpumon-flag {
  color: var(--color-muted);
}

.cpumon-flag--on {
  color: var(--color-sim-high);
  font-weight: 700;
}

/* Changed by the run since it last paused (the registers' rule). */
.cpumon-flag--changed {
  background: var(--color-surface-1);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 -2px 0 var(--color-warning);
}

.cpumon-flag--settable {
  cursor: pointer;
}

.cpumon-step {
  min-width: 2ch;
  text-align: center;
  color: var(--color-muted);
}

.cpumon-step--on {
  color: var(--color-text);
  background: var(--color-surface-1);
  border-radius: var(--radius-sm);
}

.cpumon-step-label {
  color: var(--color-subtext);
}

.cpumon-pin {
  display: inline-flex;
  gap: var(--space-1);
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
}

.cpumon-pin--active {
  background: var(--color-surface-1);
  font-weight: 700;
}

.cpumon-level--H {
  color: var(--color-sim-high);
}

.cpumon-level--L {
  color: var(--color-sim-low);
}

.cpumon-level--Z {
  color: var(--color-sim-float);
}

.cpumon-level--X {
  color: var(--color-sim-conflict);
}

.cpumon-doing {
  font-family: var(--font-sans);
  color: var(--color-text);
}

.cpumon-doing--wai,
.cpumon-doing--stp,
.cpumon-doing--halt,
.cpumon-doing--busack,
.cpumon-doing--reset {
  color: var(--color-warning);
}

/* The pipeline: five recorded, the one in flight, five decoded ahead. */
.cpumon-lines {
  display: flex;
  flex-direction: column;
}

.cpumon-line {
  display: grid;
  /* Fixed columns, so the panel is as wide as its widest line can be: the
     longest instruction text is "LD (IX+$7F),$FF" or "BBR7 $FF,$FFFF". */
  grid-template-columns: 1ch 6ch 11ch 16ch 4ch;
  gap: var(--space-2);
  line-height: 1.5;
  white-space: pre;
}

.cpumon-line--previous {
  opacity: 0.75;
}

.cpumon-line--current {
  background: var(--color-code-current);
  border-radius: var(--radius-sm);
}

.cpumon-line-mark {
  color: var(--color-warning);
}

/* The margin a click sets a breakpoint in, as the chip designer's gutter. */
.cpumon-line-mark--gutter {
  cursor: pointer;
}

.cpumon-line-mark--break {
  color: var(--color-debug-break);
}

.cpumon-line-addr {
  color: var(--color-code-keyword);
}

.cpumon-line-bytes {
  color: var(--color-muted);
}

.cpumon-line-text {
  color: var(--color-code-number);
}

.cpumon-line-mode {
  color: var(--color-muted);
}

.cpumon-line--illegal .cpumon-line-text {
  color: var(--color-muted);
  font-style: italic;
}

.cpumon-line--event .cpumon-line-text {
  grid-column: 3 / -1;
  font-family: var(--font-sans);
  color: var(--color-code-signal);
}

/* The registers, one a line. */
/* Spaced as the memory rows are (no gap, a 1.5 line): a Z80's seventeen
   must fit the window's height. */
.cpumon-regs {
  display: grid;
  grid-template-columns: max-content;
  line-height: 1.5;
}

.cpumon-reg {
  display: inline-flex;
  gap: var(--space-2);
}

.cpumon-reg-name {
  min-width: 4ch;
  color: var(--color-muted);
}

.cpumon-reg-value {
  color: var(--color-text);
}

/* Changed by the run since it last paused: a Step's instruction, or the
   stretch run to a breakpoint. */
.cpumon-reg--changed .cpumon-reg-value {
  color: var(--color-warning);
  font-weight: 700;
}

.cpumon-reg--editable .cpumon-reg-value {
  cursor: pointer;
}

.cpumon-reg--selected .cpumon-reg-value {
  outline: 2px solid var(--color-text);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}

/* The stack's top, an entry a line. */
.cpumon-stack-lines,
.cpumon-break-lines,
.cpumon-port-lines {
  display: flex;
  flex-direction: column;
  line-height: 1.5;
}

.cpumon-stack-line {
  display: flex;
  gap: var(--space-3);
  cursor: pointer;
}

.cpumon-stack-addr,
.cpumon-port-addr {
  color: var(--color-muted);
}

.cpumon-stack-value {
  color: var(--color-info);
}

.cpumon-stack-line--top .cpumon-stack-value {
  color: var(--color-text);
  font-weight: 700;
}

/* The breakpoints: a dot, where, what is there, and a × to clear it. */
.cpumon-break-line {
  display: grid;
  grid-template-columns: 1ch 5ch 14ch auto;
  align-items: center;
  gap: var(--space-2);
  white-space: pre;
}

.cpumon-break-addr {
  color: var(--color-code-keyword);
  cursor: pointer;
}

.cpumon-break-text {
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--color-code-number);
}

.cpumon-break-clear {
  padding: 0 var(--space-1);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font: inherit;
  cursor: pointer;
}

.cpumon-break-clear:hover {
  background: var(--color-surface-1);
  color: var(--color-text);
}

/* The Z80's I/O ports: what each was last given (OUT) and gave (IN). */
.cpumon-port-line {
  display: grid;
  grid-template-columns: 4ch 3ch 3ch;
  gap: var(--space-3);
}

.cpumon-port-head {
  font-family: var(--font-sans);
  font-size: var(--font-size-xs);
  color: var(--color-muted);
}

.cpumon-port-value {
  color: var(--color-info);
}

.cpumon-port-value--last {
  color: var(--color-text);
  font-weight: 700;
}

.cpumon-port-line--latest {
  background: var(--color-code-current);
  border-radius: var(--radius-sm);
}
/* Web-only navigation actions. */
.web-action {
  border: 1px solid var(--color-surface-1);
  border-radius: 6px;
  background: var(--color-surface-0);
  color: var(--color-text);
  font: inherit;
  font-size: var(--font-size-sm);
  padding: 0.45rem 0.75rem;
  cursor: pointer;
  text-decoration: none;
}

.web-action:hover {
  background: var(--color-surface-1);
}

.web-action[hidden] {
  display: none;
}

@media (pointer: coarse) {
  .wire-hit {
    stroke-width: 22;
  }

  .embed-action,
  .web-action {
    min-width: 44px;
    min-height: 44px;
  }
}

/* Example Library dialog (web edition). */
.example-library-popup {
  display: flex;
  flex-direction: column;
  width: max(560px, calc(var(--font-size) * 46));
  max-width: 92vw;
  height: max(520px, calc(var(--font-size) * 38));
  max-height: 88vh;
  overflow: hidden;
  padding: 0;
}

.example-library-popup .popup-header {
  padding: var(--space-3) var(--space-4);
  background: var(--color-mantle);
  font-weight: 700;
}

.example-library-body {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-3);
  min-height: 0;
  padding: var(--space-4);
}

.example-library-search {
  flex: none;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-surface-1);
  border-radius: var(--radius-sm);
  background: var(--color-surface-0);
  color: var(--color-text);
  font: inherit;
}

.example-library-status {
  margin: 0;
  color: var(--color-subtext);
}

.example-library-status[hidden] {
  display: none;
}

.example-library-list {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-4);
  min-height: 0;
  overflow-y: auto;
}

.example-library-category {
  margin: 0 0 var(--space-2);
  color: var(--color-muted);
  font-size: var(--font-size-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.example-library-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--color-surface-1);
  border-radius: var(--radius-sm);
}

.example-library-name {
  margin: 0;
  font-size: var(--font-size);
  font-weight: 700;
}

.example-library-desc {
  margin: 0;
  color: var(--color-subtext);
  font-size: var(--font-size-sm);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.example-library-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  justify-content: flex-end;
}

.example-library-meta {
  margin-right: auto;
  color: var(--color-muted);
  font-size: var(--font-size-sm);
}
