/* ──────────────────────────────────────────────────────────────────────
 * controlsfreak.dev — shared design system
 *
 * Every page links this file (<link rel="stylesheet" href="/styles.css">).
 * It holds the whole vocabulary: the :root custom properties (the theme —
 * change colors here, not in pages) and the component classes (.tool-card,
 * .tabs, .form-row, .result-panel, .site-nav, …). Pages should be built
 * from these, not freshly styled.
 *
 * Page-specific CSS still lives inline on the page that needs it
 * (e.g. contact.html's honeypot/intro rules) — only genuinely shared rules
 * belong here. The 11ty build templates the HTML chrome and passthrough-
 * copies this file unchanged; the browser loads it directly, no bundler or
 * preprocessor in the pipeline.
 * ────────────────────────────────────────────────────────────────────── */

/* ── THEME TOKENS — dual, DARK-DEFAULT, two registers ──────────────────
 * `:root` is the DARK theme (the default); `:root[data-theme="light"]`
 * below overrides it back to ≈ the pre-redesign look. A before-paint
 * bootstrap in _includes/head.njk sets data-theme from cf_theme, else
 * prefers-color-scheme; the nav toggle + /scripts/theme.js flip it at
 * runtime. See CLAUDE.md "Design system" for the two-register model.
 *
 * SOFTWARE REGISTER = the default chrome (carries the whole site):
 * "Niagara AX with a dark mode" — cool blue-slate, AX-SHARP (square
 * corners via --rail:0, hard 1px seams, flat fills, no floating
 * shadows). Green = brand/action; blue = data/selection.
 *
 * EQUIPMENT REGISTER = the --dev-* / --lcd-* tokens at the bottom of
 * this block: a warm device face + positive-mode dot-matrix character
 * LCD, used ONLY where a page depicts real hardware (VFD/DMM sims,
 * device widgets, the one readout in a tool that shows a field value).
 * These stay CONSTANT across themes — a device is a device either way,
 * so the light override deliberately does NOT touch them (nor --rail).
 * ──────────────────────────────────────────────────────────────────── */
:root {
    color-scheme: dark;

    /* software register — cool blue-slate (the default chrome) */
    --bg:           #13161b;   /* cool slate, near-black — the page floor */
    --bg-2:         #181c22;   /* gutter / recessed chassis */
    --surface:      #1e232b;   /* content panes / software panel face */
    --surface-2:    #283038;   /* panel headers, raised header strips, insets */
    --surface-3:    #181c22;   /* recessed reference / context panels (= --bg-2) */
    --well:         #11141a;   /* deepest recessed field (tree / wiresheet / gauge wells) */
    --border:       #3c4757;   /* cool gray-blue seam — crisp 1px edges */
    --border-faint: #2b333d;   /* lighter hairline for inner row dividers */
    --bevel-hi:     rgba(170,195,230,0.06);  /* 1px inset highlight on raised chrome */
    --bevel-lo:     rgba(0,0,0,0.45);        /* 1px shade under it */
    --accent:       #6cb23a;   /* the green — brand / action (refined for dark, not neon) */
    --accent-bright:#86cf4d;
    --accent-dim:   rgba(108,178,58,0.14);
    --accent-glow:  rgba(108,178,58,0.24);
    --accent-soft:  rgba(108,178,58,0.07);  /* one notch under --accent-dim — status-pill / inset backdrops */
    --text:         #c5cdd8;   /* cool light-gray body copy */
    --text-bright:  #eef2f8;   /* headings */
    --text-dim:     #828d9b;   /* labels, secondary text */
    --blue:         #4aa3dd;   /* data readouts / selection (Niagara steel-azure); also "supply water" in diagrams */
    --blue-bright:  #74c2f0;
    --blue-cool:    #6f97aa;   /* muted companion to --blue — "return water" in diagrams (paired with a dashed line so it reads without colour too) */
    --blue-dim:     rgba(74,163,221,0.16);
    --blue-glow:    rgba(74,163,221,0.24);
    --red:          #e85d4f;   /* fault / alarm */
    --red-dim:      rgba(232,93,79,0.14);   /* faint red wash — quiz wrong-answer fill (parallels --accent-dim) */
    --heat:         #e8884a;   /* warm orange — "heating" on the psychrometric chart's process lines; companion to --blue for cooling */
    --teal:         #46b3ac;   /* teal — data/comm traces on the background schematic (BACnet, IP) */
    --teal-dim:     rgba(70,179,172,0.14);
    --teal-glow:    rgba(70,179,172,0.24);
    --amber:        #e0a94a;   /* amber — energized control wiring on the background schematic; section accent for /practice/ */
    --amber-dim:    rgba(224,169,74,0.14);
    --amber-glow:   rgba(224,169,74,0.26);
    --plum:         #b783c0;   /* plum — logic-signal lines on the background schematic */
    --plum-dim:     rgba(183,131,192,0.14);
    --plum-glow:    rgba(183,131,192,0.26);

    /* blueprint graticule (body background) — dark gets a faint grid +
       a subtle cool radial wash; light overrides below to the stronger
       1.5px grid with no wash (preserves today's drafting-paper feel). */
    --grid-line-c:  var(--border-faint);
    --grid-line-w:  1px;
    --grid-wash:    rgba(74,163,221,0.05);

    /* equipment register — warm device face + dot-matrix LCD. CONSTANT
       across themes (the light override leaves these untouched). */
    --dev-face:     #16140f;   /* warm near-black instrument face */
    --dev-bezel:    #0c0b08;
    --dev-edge:     #2c2a20;   /* warm bezel seam */
    --dev-hi:       rgba(220,210,170,0.08);
    --lcd-bg:       #adba4e;   /* lit olive-green backlight — BMS character LCD */
    --lcd-bg-2:     #9aa840;   /* backlight gradient / vignette */
    --lcd-ink:      #1b2410;   /* dark pixel text */
    --lcd-grid:     rgba(18,26,6,0.18);  /* unlit pixel-mesh gaps */

    --mono:         'IBM Plex Mono', monospace;
    --sans:         'Overpass', sans-serif;
    --rail:         0px;       /* AX-sharp: square corners site-wide (both themes) */
}

/* LIGHT THEME — ≈ the pre-redesign look. Flips the software register
   back to warm white panels on light gray-green. The equipment
   --dev-* / --lcd-* tokens and --rail are deliberately NOT overridden. */
:root[data-theme="light"] {
    color-scheme: light;
    --bg:           #eef1ec;
    --bg-2:         #e8ece4;
    --surface:      #ffffff;
    --surface-2:    #f2f6f1;
    --surface-3:    #e8ece4;
    --well:         #e8ece4;
    --border:       #ccd7c8;
    --border-faint: #e3e8df;
    --bevel-hi:     rgba(255,255,255,0.8);
    --bevel-lo:     rgba(0,0,0,0.05);
    --accent:       #43881c;
    --accent-bright:#4a9620;
    --accent-dim:   rgba(67,136,28,0.10);
    --accent-glow:  rgba(67,136,28,0.22);
    --accent-soft:  rgba(67,136,28,0.06);
    --text:         #38423a;
    --text-bright:  #1d251f;
    --text-dim:     #666e66;
    --blue:         #1577b8;
    --blue-bright:  #1981c6;
    --blue-cool:    #5e8aa0;
    --blue-dim:     rgba(21,119,184,0.10);
    --blue-glow:    rgba(21,119,184,0.22);
    --red:          #c4382f;
    --red-dim:      rgba(196,56,47,0.10);
    --heat:         #c8782a;
    --teal:         #4a8a8a;
    --teal-dim:     rgba(74,138,138,0.10);
    --teal-glow:    rgba(74,138,138,0.22);
    --amber:        #c9a14a;
    --amber-dim:    rgba(201,161,74,0.10);
    --amber-glow:   rgba(201,161,74,0.22);
    --plum:         #8a5e7e;
    --plum-dim:     rgba(138,94,126,0.10);
    --plum-glow:    rgba(138,94,126,0.22);
    --grid-line-c:  var(--border);
    --grid-line-w:  1.5px;
    --grid-wash:    transparent;
}

/* Print always uses the LIGHT token set, regardless of the on-screen
   theme (audit-2026-06 #54: browsers drop background fills by default
   when printing, so the dark theme's light-on-dark text became
   near-white text on unprinted white paper — body 1.60:1, headings
   1.12:1, an effectively blank page). The theme is attribute-driven,
   so the browser's forced-light print mode can't flip it; this block
   does. Duplicates the light set above — keep the two in sync when
   retuning light tokens. */
@media print {
    :root, :root[data-theme="dark"] {
        color-scheme: light;
        --bg:           #eef1ec;
        --bg-2:         #e8ece4;
        --surface:      #ffffff;
        --surface-2:    #f2f6f1;
        --surface-3:    #e8ece4;
        --well:         #e8ece4;
        --border:       #ccd7c8;
        --border-faint: #e3e8df;
        --bevel-hi:     rgba(255,255,255,0.8);
        --bevel-lo:     rgba(0,0,0,0.05);
        --accent:       #43881c;
        --accent-bright:#4a9620;
        --accent-dim:   rgba(67,136,28,0.10);
        --accent-glow:  rgba(67,136,28,0.22);
        --accent-soft:  rgba(67,136,28,0.06);
        --text:         #38423a;
        --text-bright:  #1d251f;
        --text-dim:     #666e66;
        --blue:         #1577b8;
        --blue-bright:  #1981c6;
        --blue-cool:    #5e8aa0;
        --blue-dim:     rgba(21,119,184,0.10);
        --blue-glow:    rgba(21,119,184,0.22);
        --red:          #c4382f;
        --red-dim:      rgba(196,56,47,0.10);
        --heat:         #c8782a;
        --teal:         #4a8a8a;
        --teal-dim:     rgba(74,138,138,0.10);
        --teal-glow:    rgba(74,138,138,0.22);
        --amber:        #c9a14a;
        --amber-dim:    rgba(201,161,74,0.10);
        --amber-glow:   rgba(201,161,74,0.22);
        --plum:         #8a5e7e;
        --plum-dim:     rgba(138,94,126,0.10);
        --plum-glow:    rgba(138,94,126,0.22);
        --grid-line-c:  var(--border);
        --grid-line-w:  1.5px;
        --grid-wash:    transparent;
    }
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }

body {
    background-color: var(--bg);
    /* Blueprint graticule behind everything — the grid only reads in the
       gutters around/below content, since nav / hero / cards / footer all
       paint opaque surfaces over it. Token-driven per theme (the --grid-*
       tokens in :root): DARK = a faint 1px grid in --border-faint under a
       subtle cool radial wash; LIGHT = the stronger 1.5px --border grid,
       no wash (the drafting-paper weight tuned on earlier feedback — 1px /
       --border-faint was too quiet, 3-4px crossed into graph-paper
       foreground). Hidden on print via the @media print block below. */
    background-image:
        radial-gradient(130% 80% at 50% -20%, var(--grid-wash), transparent 60%),
        linear-gradient(to right,  var(--grid-line-c) var(--grid-line-w), transparent var(--grid-line-w)),
        linear-gradient(to bottom, var(--grid-line-c) var(--grid-line-w), transparent var(--grid-line-w));
    background-size: auto, 40px 40px, 40px 40px;
    color: var(--text);
    font-family: var(--sans);
    font-size: 18px;
    line-height: 1.6;
    min-height: 100vh;
    /* sticky footer: stack nav / (hero?) / main / footer in a flex column
       and let main grow, so the footer hugs the viewport bottom on short pages
       instead of leaving a big gap underneath. */
    display: flex;
    flex-direction: column;
    /* Stacking context for the gutter schematic SVGs (.schematic-bg),
       which sit at z-index:-1 — above body's bg-image grid but below
       every content surface (nav, hero, main, footer). Without
       isolation:isolate the SVGs would fall behind body's background
       and disappear entirely. */
    position: relative;
    isolation: isolate;
}
main { flex: 1; }

/* Hide the blueprint grid on print — a printable page wants a clean
   white background, not drafting paper. (Same posture browsers take
   for background-image: most won't print it by default anyway, but
   the explicit rule keeps the behavior unambiguous and disables it
   under any future print-CSS overrides too.) */
@media print {
    body { background-image: none; }
}

/* ──────────────────────────────────────────────────────────────────
 * SCHEMATIC BACKGROUND — gutter as-builts
 *
 * Two narrow vertical strips, one in each side gutter, holding
 * inline-SVG schematic motifs (piping, control wiring, function
 * blocks). Each motif draws itself in via stroke-dashoffset as it
 * scrolls into view — see /scripts/schematic-bg.js for the
 * IntersectionObserver that toggles .is-drawn.
 *
 * The strips paint at z-index:-1 inside body's stacking context, so
 * they sit above body's grid background but below every content
 * surface (nav, hero, tool-cards, footer). Width tracks the gutter
 * via clamp() so the strips stay narrow on narrow viewports and
 * never push into the 1120px content column. Below 1240px the
 * gutter gets too cramped to read — strips drop out entirely there.
 *
 * Hidden in print (matches the grid). Reduced-motion: the global
 * block at the bottom of this file collapses transition-duration
 * to 0.01ms, and schematic-bg.js draws every motif synchronously
 * on load instead of observing — so reduced-motion users see the
 * end state with no reveal. */
.schematic-bg {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: -1;
    pointer-events: none;
    /* Fill (almost) all of the gutter on wide displays so motifs scale
       up via the SVG's width:100% and read across the strip, not just
       the outer half. Capped at 340px so ultra-wide displays don't
       balloon them into competing with content. */
    width: min(340px, calc((100vw - 1180px) / 2));
    overflow: hidden;
}
.schematic-bg--left  { left: 8px; }
.schematic-bg--right { right: 8px; }

.sbg-motif {
    position: absolute;
    left: 0;
    top: var(--sbg-y, 0);
    width: 100%;
    height: auto;
    opacity: 0;
    transition: opacity 700ms ease;
}
/* `fill: none` on stroked paths is set as an inline presentation
   attribute in the partial — left over from when motifs used <use>
   shadow trees; kept inline because it costs nothing.

   Draw-in via a generous fixed dasharray. The largest motif path
   here is ~200 user units (the BACnet chip's perimeter); 600 puts
   us comfortably above that with margin to spare, so dashoffset:600
   parks every path fully invisible and the transition to 0 sweeps
   the whole path on for every motif. Three earlier approaches all
   failed:

     - `getTotalLength()`-derived `--sbg-len` CSS variable — hit
       Chromium quirks on Bezier paths and circles; they refused to
       draw fully even with offset=0.
     - `pathLength="1"` normalization on every stroke — same Chromium
       quirk on Bezier paths and circles.
     - `pathLength="1"` normalization opt-in only on safe straight
       elements (lines and L-only paths), with the bulk-fallback
       still fixed-600 — this DID render those elements correctly
       in the path-length sense, but Chromium does not honor
       pathLength="1" for `stroke-dasharray` computation: a value of
       `1` is treated as 1 actual pixel, not as 1 unit of the
       normalized path. Result: a 104px line ends up with 52
       alternating 1px dashes/gaps and renders as a near-invisible
       speckle. Verified with `getTotalLength()` returning the
       geometric length (104) while `stroke-dasharray` resolves to
       `1px` (commit b8dae2b shipped this and was reverted in the
       next commit).

   A fixed dasharray well above any path length sidesteps all three
   bugs. Trade-off accepted: drawing is no longer proportional to
   path length — short signal wires finish in ~10% of the transition
   and then sit still while long pipe runs continue drawing. The
   ease-out timing softens the disparity, and correctness beats
   uniformity here. See codebase-issues#69 for the full history and
   the revisit trigger. */
.sbg-motif [data-sbg-stroke] {
    stroke-width: 1.4;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: 600;
    stroke-dashoffset: 600;
}
.sbg-motif.is-drawn { opacity: 1; }
.sbg-motif.is-drawn [data-sbg-stroke] {
    stroke-dashoffset: 0;
    transition: stroke-dashoffset 3000ms ease-out;
}

.sbg-label {
    font-family: var(--mono);
    font-size: 9px;
    fill: var(--text-dim);
    letter-spacing: 0.04em;
}

@media (max-width: 1240px) {
    .schematic-bg { display: none; }
}
@media print {
    .schematic-bg { display: none; }
}

/* ── Skip-to-content link — keyboard users tab here first on every
       page and can Enter to jump past the nav. Positioned off-screen
       via negative top so it stays in the DOM (tabbable) but doesn't
       show; slides into view when focused. Same idiom as the well-
       known WebAIM pattern. */
.skip-link {
    position: absolute;
    top: -100px;
    left: 0.5rem;
    z-index: 200;
    padding: 0.6rem 1rem;
    background: var(--accent);
    color: var(--surface);
    font-family: var(--mono);
    font-size: 0.85rem;
    text-decoration: none;
    border-radius: 0;
    transition: top 0.15s;
}
.skip-link:focus { top: 0; outline: 2px solid var(--accent-glow); outline-offset: 2px; }

/* ── SITE NAV (every page carries the same nav markup) ── */
.site-nav {
    position: sticky;
    top: 0;
    z-index: 100;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0.9rem 2rem;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem 1.75rem;
}
.site-nav-brand {
    font-family: var(--mono);
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
    letter-spacing: 0.01em;
    text-decoration: none;
}
.site-nav-brand span {
    color: var(--accent);
    animation: breathe 4s ease-in-out infinite;
}
.site-nav-brand:hover { color: var(--text-bright); }
.site-nav-links { display: flex; flex-wrap: wrap; gap: 1.75rem; }
.site-nav-links a {
    font-family: var(--sans);
    font-size: 0.95rem;
    color: var(--text-dim);
    text-decoration: none;
    padding-bottom: 0.15rem;
    border-bottom: 2px solid transparent;
    transition: color 0.18s, border-color 0.18s;
}
.site-nav-links a:hover { color: var(--text); border-bottom-color: var(--accent); }
.site-nav-links a.active { color: var(--text-bright); border-bottom-color: var(--accent); }

/* ── NAV SECTION DROPDOWNS ───────────────────────────────────────────
   Tools / Simulators / Education each pair their top-level link with a
   disclosure toggle that opens a panel of direct links (nav-menu.js).
   The panel links are <a> inside .site-nav-links, so they'd inherit the
   nav-link rules above — the .nav-menu-scoped selectors below outrank
   them on specificity. */
.nav-item--has-menu {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}
.nav-menu-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Roomier hit area than the old fiddly 0.15rem caret, but the negative
       margin cancels the added box in layout so the link bar keeps its
       compact width (otherwise it wraps below the brand at ~1280px). */
    padding: 0.45rem 0.5rem;
    margin: -0.3rem -0.35rem;
    background: transparent;
    border: none;
    color: var(--text-dim);
    cursor: pointer;
    transition: color 0.18s;
}
.nav-menu-toggle:hover { color: var(--text); }
.nav-caret { width: 11px; height: 7px; transition: transform 0.18s; }
.nav-menu-toggle[aria-expanded="true"] .nav-caret { transform: rotate(180deg); }
.nav-menu {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 0;
    z-index: 120;
    min-width: 210px;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.nav-menu[hidden] { display: none; }
.nav-menu .nav-menu-item {
    font-family: var(--sans);
    font-size: 0.9rem;
    color: var(--text-dim);
    text-decoration: none;
    /* Taller rows — easier pointer targets than the old 0.5rem/0.85rem. */
    padding: 0.6rem 1rem;
    border-bottom: 1px solid var(--border-faint);
    white-space: nowrap;
    transition: color 0.18s, background 0.18s;
}
.nav-menu .nav-menu-item:last-child { border-bottom: none; }
.nav-menu .nav-menu-item:hover { color: var(--text-bright); background: var(--accent-dim); }
@media (prefers-reduced-motion: reduce) {
    .nav-caret { transition: none; }
}

/* ── MOBILE HAMBURGER ────────────────────────────────────────────────
   Desktop never shows it (display:none); at the narrow-nav breakpoint it
   becomes the top-bar control that collapses .site-nav-links. The two
   icons swap on aria-expanded. See nav-menu.js. */
.nav-hamburger {
    display: none;
    align-items: center;
    justify-content: center;
    padding: 0.35rem;
    background: var(--surface);
    border: 1px solid var(--border);
    color: var(--text);
    cursor: pointer;
    transition: color 0.18s, border-color 0.18s;
}
.nav-hamburger:hover { color: var(--accent); border-color: var(--accent); }
.nav-burger-icon,
.nav-close-icon { width: 18px; height: 18px; }
.nav-close-icon { display: none; }
.nav-hamburger[aria-expanded="true"] .nav-burger-icon { display: none; }
.nav-hamburger[aria-expanded="true"] .nav-close-icon { display: inline; }
/* Compact icon-only search for the mobile top bar; the full labelled
   search stays in the desktop link bar. Compound selector so it beats the
   base .nav-search-btn { display: inline-flex } regardless of source order.
   Desktop hides this one; the mobile breakpoint flips it on. */
.nav-search-btn.nav-search-btn--mobile { display: none; gap: 0; padding: 0.35rem; }

/* ── UNITS + THEME TOGGLES ───────────────────────────────────────────
   Two sibling pills inside .site-nav-links (rightmost children) — same
   shape, so they share these rules. Each active button gets the
   accent-dim tint via an attribute on <html>: [data-units] for the
   units pill, [data-theme] for the theme pill. A tiny inline head
   script sets BOTH attributes before first paint to avoid flashing the
   wrong button. Runtime drivers: /scripts/units.js (per-page) and
   /scripts/theme.js (site-wide). The button .active class is a backstop;
   the attribute selectors are the canonical drivers. */
.units-toggle,
.theme-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
}
.units-label,
.theme-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.units-toggle-pill,
.theme-toggle-pill {
    display: inline-flex;
    border: 1px solid var(--border);
    border-radius: 0;
    overflow: hidden;
    background: var(--surface);
}
.units-btn,
.theme-btn {
    font-family: var(--mono);
    font-size: 0.7rem;
    padding: 0.22rem 0.6rem;
    background: transparent;
    color: var(--text-dim);
    border: none;
    cursor: pointer;
    letter-spacing: 0.03em;
    transition: color 0.18s, background 0.18s;
}
.units-btn + .units-btn,
.theme-btn + .theme-btn { border-left: 1px solid var(--border); }
.units-btn:hover,
.theme-btn:hover { color: var(--text); }
html[data-units="us"]     .units-btn[data-units="us"],
html[data-units="metric"] .units-btn[data-units="metric"],
html[data-theme="dark"]   .theme-btn[data-theme-set="dark"],
html[data-theme="light"]  .theme-btn[data-theme-set="light"] {
    background: var(--accent-dim);
    color: var(--accent);
    font-weight: 600;
}

/* ── SITE SEARCH (command palette) ───────────────────────────────────
   The nav trigger mirrors the units/theme pill vocabulary. The palette is
   the site's only floating overlay (z-index above the sticky nav, which is
   100); it lives at end-of-body so it escapes the nav stacking context.
   Opened by / or Ctrl/⌘-K or .nav-search-btn — see scripts/search.js. */
.nav-search-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    padding: 0.26rem 0.6rem;
    background: var(--surface);
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: 0;
    cursor: pointer;
    letter-spacing: 0.03em;
    transition: color 0.18s, border-color 0.18s;
}
.nav-search-btn:hover { color: var(--text); border-color: var(--accent); }
.nav-search-icon { width: 13px; height: 13px; flex: none; }
.nav-search-kbd {
    font-family: var(--mono);
    font-size: 0.66rem;
    line-height: 1;
    padding: 0.12rem 0.3rem;
    color: var(--text-dim);
    background: var(--surface-2);
    border: 1px solid var(--border);
}

.palette {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 12vh 1rem 1rem;
    background: rgba(0, 0, 0, 0.55);
}
.palette[hidden] { display: none; }
body.palette-open { overflow: hidden; }
.palette-box {
    width: 100%;
    max-width: 560px;
    max-height: 70vh;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
    animation: palette-in 0.14s ease-out;
}
@keyframes palette-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
}
.palette-field {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.7rem 0.85rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.palette-icon { width: 16px; height: 16px; flex: none; color: var(--text-dim); }
.palette-input {
    flex: 1;
    min-width: 0;
    background: transparent;
    border: none;
    outline: none;
    font-family: var(--sans);
    font-size: 1rem;
    color: var(--text);
}
.palette-input::placeholder { color: var(--text-dim); }
.palette-hint {
    font-family: var(--mono);
    font-size: 0.66rem;
    color: var(--text-dim);
    padding: 0.12rem 0.32rem;
    border: 1px solid var(--border);
    background: var(--surface);
}
.palette-results {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
}
.palette-result {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
        "tag title"
        "tag desc";
    gap: 0 0.7rem;
    align-items: baseline;
    padding: 0.6rem 0.85rem;
    border-bottom: 1px solid var(--border-faint);
    cursor: pointer;
}
.palette-result:last-child { border-bottom: none; }
.palette-result[aria-selected="true"] { background: var(--accent-dim); }
.palette-result-tag {
    grid-area: tag;
    align-self: center;
    font-family: var(--mono);
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    white-space: nowrap;
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid var(--accent-glow);
    padding: 0.12rem 0.36rem;
}
.palette-result-title {
    grid-area: title;
    font-family: var(--sans);
    font-size: 0.95rem;
    color: var(--text-bright);
}
.palette-result-desc {
    grid-area: desc;
    font-size: 0.8rem;
    color: var(--text-dim);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}
.palette-status {
    margin: 0;
    padding: 0.5rem 0.85rem;
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    border-top: 1px solid var(--border);
    background: var(--surface-2);
}
@media (prefers-reduced-motion: reduce) {
    .palette-box { animation: none; }
}

/* ── HERO — workstation-panel frame (v3.0.0 "seam" hero).
       A bordered surface: faux BAS title bar above, live status line
       below, and the live "seam" instrument + copy between (those are
       laid out in the home page's own inline head styles, since they're
       home-only). Only the home page carries a .hero; sub-pages don't.
       The .console-* / .ok-pill chrome here is shared (nav-cards mirror
       the frame). */
.hero {
    position: relative;
    z-index: 1;
    padding: 0;
    max-width: 1120px;
    margin: 2.5rem auto 2rem;
    width: 100%;   /* don't shrink-to-fit inside body's flex column */
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    overflow: hidden;
    animation: fadeUp 0.5s ease both;
}
.console-titlebar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.55rem 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.console-titlebar .titlebar-dots {
    color: var(--text-dim);
    margin: 0 0.4rem;
}
/* Three-column grid (1fr · auto · 1fr) so the middle item lands at the
   true visual center of the card — flex + space-between distributes by
   midpoint between the outer items' inside edges, which drifts off
   true-center when the OK pill and LAST BUILT have different widths
   (and shifts the version label off the hero h1's center axis above).
   The outer 1fr cells get equal width; the middle auto cell sizes to
   content and centers itself between them. */
.console-statusline {
    padding: 0.7rem 1rem;
    border-top: 1px solid var(--border);
    background: var(--surface-2);
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 0.5rem 1.25rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
}
.console-statusline > :first-child  { justify-self: start; }
.console-statusline > :nth-child(2) { justify-self: center; }
.console-statusline > :last-child   { justify-self: end; }
.console-statusline > span strong {
    color: var(--text);
    font-weight: 600;
    margin-left: 0.35rem;
}

/* The .ok-pill: a small accent pill with a leading dot. Used in the
   title bar, status line, footer, and anywhere a "live / running"
   marker is wanted. */
.ok-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: var(--accent-dim);
    color: var(--accent);
    border: 1px solid var(--accent-glow);
    padding: 0.1rem 0.5rem;
    border-radius: 0;
    font-family: var(--mono);
    font-size: 0.62rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    font-weight: 600;
}
.ok-pill::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
}

/* ── MAIN ── */
/* The home page puts a .hero between the nav and main, so main doesn't need
   its own top padding there — .hero already provides air. Sub-pages have no
   hero, so main supplies the breathing room itself. The adjacent-sibling
   selector below zeroes out the top padding when a hero comes first. */
main {
    position: relative;
    z-index: 1;
    max-width: 1120px;
    margin: 0 auto;
    padding: 2.5rem 2rem 4rem;
    width: 100%;
}
.hero + main { padding-top: 0; }

.section-header {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    margin-bottom: 1.5rem;
}
.section-label {
    font-family: var(--mono);
    font-size: 0.82rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--accent);
    white-space: nowrap;
    margin: 0;
}
/* The trailing hairline that used to follow every section-label is
   hidden in v2.0.0 — the SEC:NNN section-number prefix on h2 labels
   carries enough left-side weight that a trailing line is noise. The
   markup stays in existing pages as a no-op so a future tweak can flip
   it back without retroactive edits; new pages can omit it. */
.section-line { display: none; }

/* h2.section-label is the section-divider variant (vs h1.section-label
   which is a landing-page title). One BAS-instrument prefix mode:

   - `data-stage="N"` renders `STAGE:N · LABEL`. Evokes equipment
     staging (Stage 1 / Stage 2 cooling, lead/lag pumps) — used on
     the home page where the sections read as a staged sequence:
     Stage 1 first, then Stage 2 if you want more, then Stage 3 for
     backstory.

   The accent color attaches to the prefix, not the label — the prefix
   becomes the wayfinding mark, the label reads as a quiet workstation
   heading. Pages without the attribute are unaffected (no prefix;
   existing accent label color preserved). */
h2.section-label[data-stage] {
    font-size: 0.78rem;
    letter-spacing: 0.06em;
    color: var(--text-bright);
}
h2.section-label[data-stage]::before {
    color: var(--accent);
    font-variant-numeric: tabular-nums;
    margin-right: 0.1rem;
    font-weight: 600;
    content: "STAGE:" attr(data-stage) " · ";
}

/* ── TOOL CARD ── */
.tool-card {
    position: relative;            /* anchor for the .tool-card-fullscreen-btn */
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    overflow: hidden;
    margin-bottom: 1.5rem;
}
/* :nth-of-type counts tool-card siblings independently of a leading
   .section-header div; nth-child counts all siblings and would shift
   indices by one on every content page. Four indices covers the longest
   tool-card stack today (4 on pid-basics: page topic + 3 mini-sims). */
.tool-card:nth-of-type(1) { animation: fadeUp 0.5s 0.08s ease both; }
.tool-card:nth-of-type(2) { animation: fadeUp 0.5s 0.16s ease both; }
.tool-card:nth-of-type(3) { animation: fadeUp 0.5s 0.24s ease both; }
.tool-card:nth-of-type(4) { animation: fadeUp 0.5s 0.32s ease both; }

.tool-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.9rem 1.5rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.tool-card-title {
    font-family: var(--mono);
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-bright);
    display: flex;
    align-items: center;
    gap: 0.7rem;
    margin: 0;
}
.tool-tag {
    font-family: var(--mono);
    font-size: 0.62rem;
    padding: 0.15rem 0.5rem;
    background: var(--accent-dim);
    color: var(--accent);
    border: 1px solid var(--accent-glow);
    border-radius: 0;
    text-transform: uppercase;
    letter-spacing: 0.07em;
}
.tool-body { padding: 1.5rem; }
.tool-body p { font-size: 0.95rem; line-height: 1.8; color: var(--text); }
.tool-body a { color: var(--accent); }
/* Body-prose lists — same font triplet as .tool-body p. Per-list margin
   stays inline (it varies). A page that wants a tighter small-text list
   overrides font-size/line-height inline; the inline style wins. */
.tool-body ul { font-size: 0.95rem; line-height: 1.8; color: var(--text); }

/* Mono small-caps preamble caption that sits under a .tool-card-header.
   Element-qualified to tie .tool-body p's (0,0,1,1) specificity and win
   on cascade order, matching p.bit-hint / p.pid-note / p.ref-note.
   Padding/margin stays per-site inline — value varies by whether the
   preamble sits outside .tool-body (between header and the next block)
   or inside .tool-body (where .tool-body's own padding applies). */
p.tool-preamble {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    line-height: 1.7;
}

/* Lead paragraph on Education pages — the opening prose paragraph that
   sets up the lesson. Slightly larger / wider line-height than the body
   .tool-body p shape. Spans the full .tool-body width so the intro
   doesn't read as marooned in the left half of the card. Placed before
   .callout p (cascade order) so .callout p's tighter font shape stays
   authoritative inside callouts. */
.page-intro {
    font-size: 1.0rem;
    line-height: 1.85;
    color: var(--text);
    margin-bottom: 1.75rem;
}

/* Lead paragraph on section landings (/tools/, /simulators/,
   /education/) — opens with a single intro paragraph above the
   filter chips / card grid. Lighter font-weight than .page-intro
   (which is for education content pages); spans the full content
   column so it visually lines up with the card grid below rather
   than reading as a half-width fragment. The accent-coloured anchor
   scoped below covers inline links in landing leads without needing
   per-link inline styles. */
.landing-intro {
    font-weight: 300;
    font-size: 1.0rem;
    color: var(--text);
    margin-bottom: 2rem;
    line-height: 1.8;
}
.landing-intro a { color: var(--accent); }

/* ── TABS ── */
.tabs {
    display: flex;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1.5rem;
}
.tab-btn {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    padding: 0.55rem 1.1rem;
    border: none;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: all 0.18s;
}
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-btn:hover:not(.active) { color: var(--text); }
.tab-pane { display: none; }
.tab-pane.active { display: block; }

/* ── FORM ── */
.form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
    margin-bottom: 1rem;
}
.field { display: flex; flex-direction: column; gap: 0.4rem; }

label,
.field-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-dim);
}

input[type="number"],
input[type="text"],
input[type="email"],
textarea,
select {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.55rem 0.75rem;
    font-family: var(--mono);
    font-size: 0.9rem;
    color: var(--text-bright);
    width: 100%;
    transition: border-color 0.18s, box-shadow 0.18s;
    -webkit-appearance: none;
}
textarea { resize: vertical; line-height: 1.6; min-height: 6rem; }
input:focus, textarea:focus, select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-dim);
}
select option { background: var(--surface); }

/* ── RESULT PANEL ── */
.result-panel {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 1.25rem 1.5rem;
    margin-top: 1rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
.result-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-dim);
    margin-bottom: 0.2rem;
}
.result-value {
    font-family: var(--mono);
    font-size: 2rem;
    font-weight: 600;
    color: var(--blue);
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    transition: color 0.18s;
}
.result-value.error { color: var(--red); }
.result-value.muted { color: var(--text-dim); }

.result-formula {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--border);
    font-style: italic;
    word-break: break-all;
}
/* .result-formula modifiers — formula blocks that need different layout
   chrome than the default. .flush drops the top margin and border so the
   formula sits flat under a tabbed Output panel (bacnet, signal-scaling
   slope/offset, thermistor). .wrap relaxes the default break-all so
   long readable expressions break on word boundaries instead of
   mid-token (signal-scaling Forward + Reverse panels). */
.result-formula.flush { margin: 0; padding: 0.7rem 1.25rem; }
.result-formula.wrap  { word-break: normal; overflow-wrap: break-word; }

/* ── RANGE BAR ── */
.range-bar-wrap { margin-top: 1rem; }
.range-bar-labels {
    display: flex;
    justify-content: space-between;
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 0.35rem;
}
.range-bar {
    height: 5px;
    background: var(--border);
    border-radius: 0;
    overflow: hidden;
}
.range-bar-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--blue));
    border-radius: 0;
    transition: width 0.2s ease;
    width: 0%;
}

/* ── BIT VIEWER ── */
/* p.bit-hint (not .bit-hint) so the rule ties .tool-body p on
   specificity and wins via cascade order. All current uses are on
   `<p>` — see also p.pid-note and p.ref-note below. */
p.bit-hint {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 0.85rem;
}

.bit-grid {
    display: grid;
    grid-template-columns: repeat(16, 1fr);
    gap: 4px;
    margin-bottom: 1rem;
}

.bit {
    aspect-ratio: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0;
    cursor: pointer;
    transition: all 0.12s;
    user-select: none;
    /* The cells are real <button>s now (keyboard/AT access) — undo the
       UA button box so the grid metrics don't shift. */
    padding: 0;
    font: inherit;
}
.bit:hover { border-color: var(--text-dim); }
.bit.on { background: var(--accent-dim); border-color: var(--accent); }

.bit-val {
    font-family: var(--mono);
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-dim);
    line-height: 1;
}
.bit.on .bit-val { color: var(--accent); }
.bit-idx {
    font-family: var(--mono);
    font-size: 0.48rem;
    color: var(--text-dim);
    opacity: 0.45;
    margin-top: 2px;
}

.bit-readouts {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.75rem;
}
.readout {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.65rem 0.85rem;
}
.readout-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-dim);
    margin-bottom: 0.25rem;
}
.readout-value {
    font-family: var(--mono);
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-bright);
    word-break: break-all;
    font-variant-numeric: tabular-nums;
}

/* ── FOOTER — v2.0.0 status-bar treatment.
       Mono caps, --surface-2 background, an [OK] pill + site name on the
       left and version / build datestamp / heartbeat dot on the right.
       The dot pulses on a 2-second keyframe — GPU-cheap, honored by
       prefers-reduced-motion via the global block at the bottom of this
       file. */
footer {
    position: relative;
    z-index: 1;
    border-top: 1px solid var(--border);
    background: var(--surface-2);
    padding: 0.85rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;   /* full-bleed status bar — matches .site-nav */
    gap: 1rem;
}
.footer-left,
.footer-right {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.footer-text {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-variant-numeric: tabular-nums;
}
.footer-text strong {
    color: var(--text);
    font-weight: 600;
}
.heartbeat-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent);
    animation: heartbeat 2s ease-in-out infinite;
}

/* ── ANIMATIONS ── */
@keyframes fadeUp {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: translateY(0); }
}
/* v2.0.0 — `heartbeat` powers the footer dot and the About-card status
   row's live indicator. 2-second cycle, eased — a quiet "this system is
   running" cue. `breathe` is the slower 4-second version used on the
   .site-nav-brand span (.dev) — peripheral, never the focal point.
   Both are squashed by the global prefers-reduced-motion block at the
   bottom of this file. */
@keyframes heartbeat {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.35; }
}
@keyframes breathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

/* ── RESPONSIVE ──
   Source order is load-bearing: an override in here only wins against
   base rules defined ABOVE this block (equal specificity — last wins).
   Components defined later in the file keep their narrow-width
   overrides in a per-component @media block next to their base rules
   (.pid-controls, .sim-canvas-wrap, .sim-legend, .ref-table); do the
   same for any new late-defined component instead of adding it here. */
@media (max-width: 620px) {
    .form-row        { grid-template-columns: 1fr; }
    .bit-grid        { grid-template-columns: repeat(8, 1fr); }
    .bit-readouts    { grid-template-columns: 1fr 1fr; }
    .site-nav        { padding: 0.7rem 1rem; }
    main             { padding-left: 1rem; padding-right: 1rem; }
    .hero            { margin-left: 1rem; margin-right: 1rem; width: auto; }
    .console-titlebar,
    .console-statusline { padding: 0.5rem 0.75rem; font-size: 0.62rem; }
    footer           { padding: 0.85rem 1rem; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
    /* Long section-header labels would otherwise force horizontal scroll
       on narrow viewports because of the desktop white-space:nowrap.
       Allow wrapping below the mobile breakpoint — the trailing
       .section-line still sits centered alongside the wrapped label. */
    .section-label   { white-space: normal; }
    /* Collapse the whole link bar behind the hamburger so the header isn't
       half the screen. Top bar = brand + search + hamburger; the links sheet
       drops to its own full-width column below, only when .nav-open. */
    .site-nav-brand { margin-right: auto; }   /* group search + burger at right */
    .nav-hamburger { display: inline-flex; }
    .nav-search-btn.nav-search-btn--mobile { display: inline-flex; }
    /* The full search lives in the sheet on desktop; on mobile the top-bar
       icon button covers it, so drop the in-sheet copy to avoid two. */
    #nav-search-btn { display: none; }
    .site-nav-links {
        display: none;
        order: 3;                 /* after the search btn + hamburger */
        flex-basis: 100%;         /* its own row when shown */
        flex-direction: column;
        /* Override the desktop flex-wrap:wrap — with the capped height a
           wrapping column breaks into a SECOND column off to the right,
           which was the sideways-scroll. Keep it one scrolling column. */
        flex-wrap: nowrap;
        align-items: stretch;
        gap: 0.15rem;
        margin-top: 0.5rem;
    }
    /* Cap the open sheet to the space under the top bar and give it its own
       smooth scroll. Without this the sticky .site-nav grows taller than the
       viewport on long lists, so the page scrolls behind the pinned bar
       instead of the menu — the jumpy, unresponsive scrolling. dvh tracks
       the real mobile viewport (vh is the fallback); overscroll-behavior
       stops the scroll from chaining to the page at the ends. */
    .site-nav.nav-open .site-nav-links {
        display: flex;
        max-height: calc(100vh - 5rem);
        max-height: calc(100dvh - 5rem);
        /* overflow-x must be explicit: setting only overflow-y:auto makes
           the browser compute overflow-x to auto too, which let the
           nowrap menu names scroll the sheet sideways. */
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior: contain;
    }
    /* Belt-and-suspenders: with the sheet open, the page itself doesn't
       scroll — only the sheet does. Mobile-scoped so a resize past the
       breakpoint releases the lock. */
    body.nav-sheet-open { overflow: hidden; }
    /* Inside the open sheet each section + its sub-menu stacks full width. */
    .nav-item--has-menu { flex-wrap: wrap; width: 100%; }
    .nav-menu {
        position: static;
        z-index: auto;
        min-width: 0;
        width: 100%;
        box-shadow: none;
        margin-top: 0.3rem;
    }
    /* Long tool names wrap in the full-width sheet instead of running off
       the side (the desktop dropdown keeps them on one line). */
    .nav-menu .nav-menu-item { white-space: normal; }
}
/* ── COPY BUTTON ── */
.copy-btn {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.2rem 0.55rem;
    background: none;
    border: 1px solid var(--border);
    border-radius: 0;
    color: var(--text-dim);
    cursor: pointer;
    transition: all 0.15s;
    white-space: nowrap;
}
.copy-btn:hover { border-color: var(--accent); color: var(--accent); }
.copy-btn.copied { border-color: var(--blue); color: var(--blue); }
.copy-btn.active { border-color: var(--accent); color: var(--accent); }

/* ── FULLSCREEN MODE ──
   Toggle button in the .tool-card corner pins the card to the viewport via
   .is-fullscreen. Wired by /scripts/fullscreen-toggle.js — pages opt in by
   adding a [data-fullscreen-target] button anywhere in the card. ESC exits.
   Add the .fs-desktop-only modifier on tools that don't make sense below
   the 1000px breakpoint (e.g. drag-wiring editors). */
.tool-card-fullscreen-btn {
    position: absolute;
    top: 0.55rem;
    right: 0.55rem;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-family: var(--mono);
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    padding: 0.32rem 0.6rem 0.32rem 0.55rem;
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 0;
    color: var(--accent);
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.tool-card-fullscreen-btn:hover { background: var(--accent-soft); }
.tool-card-fullscreen-btn svg { width: 0.85rem; height: 0.85rem; display: block; }
.tool-card-fullscreen-btn .fs-icon-exit,
.tool-card-fullscreen-btn .fs-label-exit { display: none; }
.tool-card.is-fullscreen .tool-card-fullscreen-btn .fs-icon-enter,
.tool-card.is-fullscreen .tool-card-fullscreen-btn .fs-label-enter { display: none; }
.tool-card.is-fullscreen .tool-card-fullscreen-btn .fs-icon-exit  { display: block; }
.tool-card.is-fullscreen .tool-card-fullscreen-btn .fs-label-exit { display: inline; }
@media (max-width: 999px) {
    .tool-card-fullscreen-btn.fs-desktop-only { display: none; }
}

.tool-card.is-fullscreen {
    position: fixed;
    inset: 0;
    z-index: 300;                  /* above .site-nav (100) and .skip-link (200) */
    margin: 0;
    border: 0;
    border-radius: 0;
    max-width: none;
    overflow: auto;
}
/* The "← All tools" / "← All simulators" back-link sits as a sibling of
   .tool-card inside <main>. Hide it when its preceding sibling is the
   fullscreen card — back-navigation makes no sense in fullscreen. */
.tool-card.is-fullscreen + .back-link,
.tool-card.is-fullscreen ~ .back-link { display: none; }

body.has-fullscreen-tool { overflow: hidden; }
/* In fullscreen the .tool-card overlay covers everything, but leave the
   nav and footer in place in the DOM — they're hidden by the overlay,
   not removed. The <main> element creates a stacking context of its own
   (position: relative + z-index: 1) so a fixed-positioned card inside it
   can't out-rank .site-nav (z-index: 100) on its own merits — its z-index
   only applies *within* main's context. Lift main above the nav (and
   footer, z-index: 1) while a tool is fullscreen. */
body.has-fullscreen-tool main { z-index: 400; }

/* ── SUBHEAD (within a tool body) ── */
.subhead {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--accent);
    margin: 2rem 0 0.9rem;
}
.subhead:first-child { margin-top: 0; }

/* ── PID SIM ── */
.pid-controls {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    gap: 1.25rem 1.5rem;
    margin-bottom: 1.25rem;
}
/* Phone: one full-width column so the three tuning sliders get real
   track length + hit height. Lives here, not in the RESPONSIVE block —
   that block sits earlier in the file and would lose on source order. */
@media (max-width: 620px) {
    .pid-controls { grid-template-columns: 1fr; }
}
.slider-field { display: flex; flex-direction: column; gap: 0.45rem; }
.slider-head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.15rem 0.5rem;
}
.slider-val { font-family: var(--mono); font-size: 0.8rem; color: var(--accent); }
.slider-sub {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    letter-spacing: 0.02em;
}

input[type="range"] {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 4px;
    background: var(--border);
    border-radius: 0;
    outline: none;
    cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    box-shadow: 0 0 0 1px var(--accent);
}
input[type="range"]::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--accent);
    border: 2px solid var(--bg);
    box-shadow: 0 0 0 1px var(--accent);
}

/* ── SCREEN-READER-ONLY UTILITY ──
   Text for assistive tech that must not paint — the standard
   clip-rect recipe (display:none / visibility:hidden would silence
   the screen reader too). First consumer: the pid-tuner's throttled
   live region for its three metrics readouts. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ── FOCUS INDICATORS ──
   Every custom-styled interactive with a :hover needs a paired
   :focus-visible — the browser default outline is suppressed by the
   `outline: none` on .skip-link / inputs / the range track. Add new
   custom interactives to this block. */
.copy-btn:focus-visible,
.tab-btn:focus-visible,
.units-btn:focus-visible,
.theme-btn:focus-visible,
.nav-search-btn:focus-visible,
.nav-hamburger:focus-visible,
.nav-menu-toggle:focus-visible,
.nav-menu-item:focus-visible,
.cta-button:focus-visible,
.hero-btn:focus-visible,
.hero-qt:focus-visible,
.back-link:focus-visible,
.filter-chip:focus-visible,
.nav-card:focus-visible,
.related-links a:focus-visible,
.tool-card-fullscreen-btn:focus-visible,
.quiz-choice:focus-visible,
.quiz-action:focus-visible,
.quiz-reset-best:focus-visible,
.quiz-restart-now:focus-visible,
.bit:focus-visible,
.psy-pill:focus-visible,
#th-rt-scroll:focus-visible,
#fbe-canvas:focus-visible,
.pid-spoiler > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;   /* v2.0.0 — was 2px; lifted so the accent ring no longer kisses the border on .nav-card */
}
input[type="range"]:focus-visible::-webkit-slider-thumb {
    box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px var(--accent-glow);
}
input[type="range"]:focus-visible::-moz-range-thumb {
    box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px var(--accent-glow);
}

/* ── TOUCH-TARGET FLOOR ──
   On touch devices (coarse pointer, no hover), the interactive
   controls are padded to a >=44px hit area (WCAG 2.5.5 / Apple HIG).
   Scoped to (hover: none) so pointer users keep the compact desktop
   workstation density. The .ps-input fields and .quiz-choice answers
   clear 44px natively; everything else measured under the floor lives
   here — including .copy-btn (22px natively — the audit-2026-06 #24
   measurement disproved this block's old "copy/submit buttons already
   clear 44px" claim), .filter-chip (28px), .quiz-action (39px), and
   the fullscreen button (26px). Like FOCUS INDICATORS above, this is
   the single consolidated home for the floor — add new interactives
   here rather than scattering per-component min-heights.
   See codebase-issues #74 / ux-audit #3 / #8 / #9 + audit-2026-06
   #24 / #25 for the why. */
@media (hover: none) {
    .site-nav-links a,
    .units-btn,
    .theme-btn,
    .nav-search-btn,
    .nav-menu-toggle,
    .nav-menu-item,
    .hero-qt,
    .tab-btn,
    .copy-btn,
    .filter-chip,
    .tool-card-fullscreen-btn,
    .quiz-action,
    .quiz-reset-best {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
    }
    /* The burger gets min-height ONLY — sharing the display
       declaration above overrode its base display:none at ALL widths
       under hover:none, rendering a stray non-functional hamburger
       beside the full link bar on touch tablets (audit-2026-06 #25).
       The ≤620px media block is what reveals it. */
    .nav-hamburger { min-height: 44px; }
    .site-nav-links a { padding-inline: 0.2rem; }
    .quiz-settings-select { min-height: 44px; }
    .pid-spoiler > summary { min-height: 44px; }
    /* Range sliders: the input element IS the painted 4px track (its
       background is the track color), so bare padding would paint a
       fat gray bar. border-box height 44 − 2×20 padding leaves a 4px
       content row; background-clip keeps the paint there while the
       whole 44px box takes the touch (audit #24 measured 4px-tall hit
       boxes on the PID tuning sliders). */
    input[type="range"] {
        height: 44px;
        padding-block: 20px;
        background-clip: content-box;
    }
    /* Dropdown items are full-width rows, not compact chrome — keep them
       left-aligned while still clearing the 44px floor above. */
    .nav-menu .nav-menu-item { justify-content: flex-start; }
}

/* default top spacing matches the dominant calc/converter shape:
   data row above, button row below. The .ps-row exception keeps
   toggle groups (Look up by, Chart range) vertically centered with
   their label sibling — .ps-row's grid + align-items: center would
   otherwise offset the buttons by ~0.5rem. */
.btn-row { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem; }
.ps-row > .btn-row { margin-top: 0; }

.sim-canvas-wrap {
    position: relative;            /* anchor for absolute-positioned overlays (e.g. psychrometric chart's state-point chip) */
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.75rem 0.5rem;
    margin-bottom: 0.75rem;
}
.sim-canvas-wrap canvas { display: block; width: 100%; height: 260px; }
@media (max-width: 620px) {
    .sim-canvas-wrap canvas { height: 200px; }
}
/* PID playhead — a no-padding relative box so the overlay cursor canvas
   covers #pid-canvas with a shared (0,0) origin (no border/padding offset
   math). The overlay is sized to the chart's box; its backing buffer is set
   in JS to clientWidth*dpr. */
.pid-canvas-stack { position: relative; }
.pid-canvas-stack #pid-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
}

/* ── PID equipment strip — the live process visual above the chart:
   controller (output gauge) → actuator/process scene → sensor (live PV),
   with a feedback line beneath. aria-hidden chrome; the chart + metrics
   carry the accessible story. Responsive stacking is in the breakpoints. */
.pid-eq-strip {
    background: var(--surface);
    border: 1px solid var(--border);
    padding: 0.85rem 0.75rem 0.55rem;
    margin-bottom: 0.75rem;
}
.pid-eq-row { display: flex; align-items: center; gap: 0.85rem; }
.pid-eq-controller,
.pid-eq-sensor { flex: none; width: 160px; }
.pid-eq-scene-host { flex: 1; min-width: 0; }
.pid-eq-scene { display: block; width: 100%; height: auto; }
.pid-eq-feedback {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 0.55rem;
    font-family: var(--mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.pid-eq-feedback .pid-eq-fb-line { flex: 1; border-top: 1px dashed var(--blue); }
/* The arrow points back along the strip: left while the loop lays out
   horizontally, up once the ≤700px breakpoint stacks it vertically
   (controller on top, sensor at the bottom). */
.pid-eq-fb-arrow::before { content: "← "; }
@media (max-width: 700px) {
    .pid-eq-fb-arrow::before { content: "↑ "; }
}

/* ── PID "loop details" spoiler — native <details> so you can tune the
   process by feel first, then reveal the loop-speed numbers + cheat sheets
   to check your read. Summary spans the full width over the body padding. */
.pid-spoiler {
    border: 1px solid var(--border);
    background: var(--surface);
    margin: 1.75rem 0 0;
    padding: 0 0.95rem 0.95rem;
}
.pid-spoiler > summary {
    margin: 0 -0.95rem;
    padding: 0.7rem 0.95rem;
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-bright);
}
.pid-spoiler > summary::-webkit-details-marker { display: none; }
.pid-spoiler > summary::before { content: "▸"; color: var(--accent); }
.pid-spoiler[open] > summary::before { content: "▾"; }
.pid-spoiler[open] > summary { border-bottom: 1px solid var(--border); margin-bottom: 0.4rem; }
.pid-spoiler > summary:hover { color: var(--accent); }
.pid-spoiler-tag {
    margin-left: auto;
    color: var(--amber);
    border: 1px solid var(--amber);
    border-radius: var(--rail);
    padding: 0.04rem 0.4rem;
    font-size: 0.58rem;
    letter-spacing: 0.1em;
}

/* Equipment strip — stack the controller → scene → sensor row vertically on
   phones (the horizontal loop would otherwise crowd or sideways-scroll). The
   viewBox SVGs scale down with width; the chart's own narrow-width note already
   covers the canvas below. */
@media (max-width: 700px) {
    .pid-eq-row { flex-direction: column; align-items: stretch; gap: 0.6rem; }
    .pid-eq-controller,
    .pid-eq-sensor { width: auto; }
}
.sim-legend {
    display: flex;
    gap: 1.5rem;
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 1rem;
    padding-left: 0.25rem;
}
.sim-legend .swatch {
    display: inline-block;
    width: 18px;
    height: 0;
    vertical-align: middle;
    margin-right: 0.45rem;
}
.sim-legend .swatch.pv { border-top: 2px solid var(--accent); }
.sim-legend .swatch.sp { border-top: 2px dashed var(--text-dim); }
@media (max-width: 620px) {
    .sim-legend { gap: 1rem; }
}

.readout-value.warn { color: var(--red); }

/* ── REFERENCE TABLE (property-sheet style) ── */
.ref-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
.ref-table th, .ref-table td {
    text-align: left;
    padding: 0.6rem 0.75rem;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
    line-height: 1.55;
}
.ref-table thead th {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-dim);
    font-weight: 400;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
}
.ref-table tbody tr:nth-child(even) { background: var(--surface-2); }
.ref-table tbody tr:hover { background: var(--blue-dim); }
.ref-table td:first-child { color: var(--text-bright); }
.ref-table tbody tr:last-child td { border-bottom: none; }
@media (max-width: 620px) {
    .ref-table       { font-size: 0.76rem; }
    .ref-table th, .ref-table td { padding: 0.5rem 0.5rem; }
}
/* Scroll guard for tables whose min-content width can exceed a narrow
   card (e.g. the pid-tuner spoiler's 4-column reference at <375px
   viewports): the wrapper scrolls instead of the table slicing through
   the card border. Inert (no scrollbar) wherever the table fits. */
.table-scroll { overflow-x: auto; }

/* ── CALLOUT CARDS (PID-term cards, worked-example asides, discovery
   prompts, Easter-egg reveals — same visual treatment everywhere) ── */
.callout-grid { display: grid; gap: 1rem; }
.callout {
    background: var(--bg);
    border: 1px solid var(--border);
    border-left: 2px solid var(--accent);
    border-radius: 0;
    padding: 1rem 1.25rem;
}
.callout h3 {
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--text-bright);
    margin: 0 0 0.45rem;
    font-weight: 600;
}
.callout h3 span { color: var(--text-dim); font-weight: 400; }
.callout p { font-size: 0.86rem; line-height: 1.7; }
.callout em { color: var(--text-bright); font-style: normal; }

/* ── NARROW-WIDTH ORIENTATION NOTE — hidden by default; pages flip to
   display:block inside their own @media at whatever breakpoint their
   layout shifts. Frames a layout change as mental-model orientation,
   NOT as apology. Voice reference: function-block-editor.html. ── */
.narrow-width-note {
    display: none;
    font-size: 0.8rem;
    color: var(--text-dim);
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.6rem 0.8rem;
    margin: 0.6rem 0;
}

p.pid-note {
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--text-dim);
    line-height: 1.75;
    margin-top: 1.25rem;
}

/* ──────────────────────────────────────────────────────────────────────
 * LANDING-PAGE CARDS — the clickable tiles on the home page and the
 * Tools / Education landings. Link-shaped cards with a tag, title,
 * and short description.
 * ────────────────────────────────────────────────────────────────────── */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem;
    margin-bottom: 2.5rem;
}
.card-grid.two { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* Filter chip row above the .card-grid on /tools/ and /education/.
   Each chip toggles which .nav-card[data-category] is visible; URL hash
   tracks state. Chips are <button>s with a mono pill shape — same
   family as the .nav-card-pill statusline items, just larger and
   stateful. */
.filter-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 1.5rem;
}

/* ── CHIP-ROW PREAMBLE ── Soft one-liner above a .filter-chips row.
   Element-qualified p. per site convention so .tool-body p doesn't
   out-cascade it. Mirrors p.bit-hint's shape (small mono dim text)
   with tighter bottom-margin since it sits flush above the chips. */
p.chip-row-preamble {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 0.5rem;
}
.filter-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.35rem 0.75rem;
    cursor: pointer;
    transition: border-color 0.18s, background 0.18s, color 0.18s;
}
.filter-chip:hover { border-color: var(--accent); color: var(--accent); }
.filter-chip.active {
    color: var(--accent);
    background: var(--accent-dim);
    border-color: var(--accent-glow);
}
.filter-chip-count {
    font-size: 0.65rem;
    color: var(--text-dim);
}
.filter-chip.active .filter-chip-count { color: var(--accent); opacity: 0.75; }
/* The .nav-card rule below sets display:block, which would override the UA
   `hidden { display: none }` and leave hidden cards consuming grid slots.
   Re-assert display: none for the [hidden] case. */
.nav-card[hidden] { display: none; }

/* v2.1.0 — nav cards moved from a flat surface+padding shape to a
   faux-instrument frame that mirrors the home hero (.console-titlebar
   + body + .console-statusline). The outer .nav-card supplies the
   chrome; the inner .nav-card-body carries the prose. Section accent
   (--section-accent) defaults to --accent and is overridden by the
   --tools / --education / --simulators / --home modifier so the
   hover border, name color, and status pill all track the section's
   identity color (matches the gutter motif palette). See
   _includes/nav-card.njk for the macro that emits the chrome — pages
   call it instead of hand-rolling the markup. */
.nav-card {
    --section-accent:      var(--accent);
    --section-accent-dim:  var(--accent-dim);
    --section-accent-glow: var(--accent-glow);
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.18s, transform 0.18s;
}
.nav-card--tools      { --section-accent: var(--accent); --section-accent-dim: var(--accent-dim); --section-accent-glow: var(--accent-glow); }
.nav-card--education  { --section-accent: var(--plum);   --section-accent-dim: var(--plum-dim);   --section-accent-glow: var(--plum-glow); }
.nav-card--simulators { --section-accent: var(--teal);   --section-accent-dim: var(--teal-dim);   --section-accent-glow: var(--teal-glow); }
.nav-card--practice   { --section-accent: var(--amber);  --section-accent-dim: var(--amber-dim);  --section-accent-glow: var(--amber-glow); }
.nav-card--home       { --section-accent: var(--accent); --section-accent-dim: var(--accent-dim); --section-accent-glow: var(--accent-glow); }

.nav-card:hover { border-color: var(--section-accent); transform: translateY(-2px); }

/* Card chrome — sized down from the hero (.console-titlebar /
   .console-statusline) so a grid of cards stays readable. Smaller
   padding, smaller font, same vocabulary. */
.nav-card-titlebar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    padding: 0.45rem 0.85rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
    font-family: var(--mono);
    font-size: 0.62rem;
    color: var(--text);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
/* Title text must single-line — the 4-col card grid at 1920px viewport
   only leaves ~195px for the title beside the right-side status pill,
   and a wrapped titlebar pushes the body down inconsistently across
   the grid. titleShort values in the partial are trimmed to fit; this
   ellipsis safety catches anything that still spills over (a long
   future name, an unforeseen viewport). `min-width:0` is required for
   text-overflow to take effect inside a flex parent — without it the
   span won't shrink below its intrinsic width. */
.nav-card-titlebar > span:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.nav-card-titlebar .titlebar-dots {
    color: var(--text-dim);
    margin: 0 0.35rem;
}
.nav-card-titlebar .ok-pill {
    /* Per-section status-pill tint. Same .ok-pill shape as the hero;
       the colors come from the .nav-card--<section> tokens above. */
    background: var(--section-accent-dim);
    color: var(--section-accent);
    border-color: var(--section-accent-glow);
}
.nav-card-titlebar .ok-pill::before {
    background: var(--section-accent);
}

.nav-card-body {
    padding: 1.1rem 1.2rem;
    flex: 1;
}

.nav-card-statusline {
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--border);
    background: var(--surface-2);
    font-family: var(--mono);
    font-size: 0.6rem;
    color: var(--text-dim);
    letter-spacing: 0.07em;
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
}
.nav-card-pill {
    display: inline;
}
.nav-card-pill:not(:last-child)::after {
    content: '•';
    margin: 0 0.35rem;
    color: var(--border);
}

.nav-card-name {
    font-family: var(--mono);
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-bright);
    margin-bottom: 0.4rem;
    transition: color 0.18s;
}
.nav-card:hover .nav-card-name { color: var(--section-accent); }
.nav-card-desc {
    font-size: 0.95rem;
    color: var(--text);
    line-height: 1.65;
}

/* ──────────────────────────────────────────────────────────────────────
 * ABOUT-CARD REBUILD — v2.0.0 home page only
 * The home's About panel reframes the wall-of-prose pattern as a BAS-
 * inspector view: a property-sheet header (role / location / industry /
 * verified / status) followed by a recessed prose block (`.about-prose`)
 * with a blue left rule — the same shape as `.widget-anecdote`. The
 * `.live` modifier on a `.ps-value` adds a pulsing accent dot to signal
 * a "running" state, reused from the same `heartbeat` keyframe that
 * drives the footer dot.
 * Only `html/index.html` uses these today. Internals here are the
 * decoration layered on top of the shared `.ps-row` / `.ps-label` /
 * `.ps-value` vocabulary.
 * ────────────────────────────────────────────────────────────────────── */
.about-rebuild .ps-row {
    padding: 9px 1.25rem;
}
.about-rebuild .ps-row:first-child { padding-top: 1rem; }
.about-rebuild .ps-row:last-of-type {
    border-bottom: 1px solid var(--border);
}
.about-rebuild .ps-value.live {
    color: var(--accent);
    font-weight: 600;
}
.about-rebuild .ps-value.live::before {
    content: "";
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    margin-right: 0.4rem;
    vertical-align: middle;
    animation: heartbeat 2s ease-in-out infinite;
}
.about-prose {
    margin: 1rem 1.25rem 1.25rem;
    padding: 0.85rem 1rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-left: 3px solid var(--blue);
    border-radius: 0;
    font-size: 0.92rem;
    line-height: 1.75;
    color: var(--text);
}
.about-prose p + p { margin-top: 0.85rem; }
.about-prose a { color: var(--accent); }
.about-prose .colophon {
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--text-dim);
    margin-top: 1rem;
}

/* ──────────────────────────────────────────────────────────────────────
 * BACK LINK — the small "← All tools" / "← Back to …" anchor that lives
 * under a tool's .tool-card. Kept as its own class so the size and color
 * are consistent across pages (and easy to bump from one place).
 * ────────────────────────────────────────────────────────────────────── */
.back-link {
    display: inline-block;
    margin-top: 1.75rem;
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--accent);
    text-decoration: none;
    transition: color 0.18s;
}
.back-link:hover { color: var(--text-bright); }

/* ──────────────────────────────────────────────────────────────────────
 * RELATED LINKS — end-of-page cross-links to topically related tools,
 * simulators, and lessons. SEO topic-clustering signal that also helps
 * a reader navigate sideways instead of bouncing back to a landing.
 * Rendered by html/_includes/related-links.njk; sits inside <main>
 * directly above .back-link.
 * ────────────────────────────────────────────────────────────────────── */
.related-links {
    margin-top: 2rem;
    padding: 1.1rem 1.5rem 1.2rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 1.1rem 2.5rem;
}
.related-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--accent);
    margin: 0 0 0.6rem;
}
.related-links ul {
    list-style: none;
    padding: 0;
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.7;
}
.related-links li {
    padding-left: 0.95rem;
    position: relative;
}
.related-links li::before {
    content: "→";
    position: absolute;
    left: 0;
    color: var(--accent);
    font-family: var(--mono);
}
.related-links a {
    color: var(--text);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
    transition: color 0.18s, border-color 0.18s;
}
.related-links a:hover {
    color: var(--accent);
    border-color: var(--accent);
}

/* ──────────────────────────────────────────────────────────────────────
 * PRACTICE / QUIZ — the engine in /scripts/quiz-engine.js renders the
 * shell described here inside the page's <div id="quiz"></div>. Pages
 * provide only the empty container + the questions array + the call to
 * Quiz.mount. The accent for the Practice section comes from --amber
 * (see .nav-card--practice above).
 *
 * Visual vocabulary: choices are .quiz-choice buttons styled in the
 * same family as .filter-chip / .units-btn — hairline border, mono
 * caps off here since choice text is sentence-case. Selected and post-
 * reveal correct / wrong states get accent / red borders. Reveal panel
 * borrows the .callout left-bar treatment with --correct (accent) and
 * --incorrect (red) variants. Results card uses .ref-table-dense for
 * the miss list to match the rest of the site's reference-table look.
 * ────────────────────────────────────────────────────────────────────── */

.quiz { display: block; }

.quiz-settings {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem 1rem;
    margin-bottom: 1.25rem;
    padding-bottom: 0.85rem;
    border-bottom: 1px solid var(--border-faint);
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--text-dim);
}
.quiz-settings-label {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}
.quiz-settings-select {
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.2rem 0.5rem;
}
.quiz-best-readout {
    margin-left: auto;          /* push to the right on wide; wraps below on narrow */
    font-size: 0.74rem;
    letter-spacing: 0.04em;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.quiz-reset-best {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.25rem 0.6rem;
    cursor: pointer;
    transition: border-color 0.18s, color 0.18s;
}
.quiz-reset-best:hover { border-color: var(--accent); color: var(--accent); }

.quiz-dirty-notice {
    margin: 0 0 1rem;
    padding: 0.55rem 0.85rem;
    background: var(--amber-dim);
    border: 1px solid var(--amber-glow);
    border-radius: 0;
    font-family: var(--mono);
    font-size: 0.74rem;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.quiz-restart-now {
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--amber);
    background: transparent;
    border: 1px solid var(--amber);
    border-radius: 0;
    padding: 0.2rem 0.55rem;
    cursor: pointer;
    transition: color 0.18s, background 0.18s;
}
.quiz-restart-now:hover { color: var(--surface); background: var(--amber); }
/* The dirty-notice and numeric rows below set display: flex / inline-flex,
   which would override the UA-default `[hidden] { display: none }`. Same
   trap that .nav-card[hidden] solves above — re-assert hidden for the
   elements the engine toggles via the `hidden` attribute. */
.quiz-dirty-notice[hidden],
.quiz-numeric[hidden] { display: none; }

.quiz-progress {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-bottom: 1.4rem;
}
.quiz-progress-text {
    font-family: var(--mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    margin: 0;
}
.quiz-progress-bar {
    height: 4px;
    background: var(--surface-3);
    border-radius: 0;
    overflow: hidden;
}
.quiz-progress-fill {
    height: 100%;
    width: 0%;
    background: var(--amber);
    /* Global @media (prefers-reduced-motion: reduce) below squashes
       transitions to 0.01ms — no defensive selector needed here. */
    transition: width 220ms ease;
}

.quiz-question-pane {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
}
.quiz-prompt {
    font-size: 1.05rem;
    line-height: 1.7;
    color: var(--text);
}
.quiz-prompt code {
    font-family: var(--mono);
    font-size: 0.92em;
    background: var(--surface-3);
    padding: 0.05em 0.35em;
    border-radius: 0;
}

.quiz-snippet-slot { margin: 0; }
.quiz-snippet {
    font-family: var(--mono);
    font-size: 0.82rem;
    line-height: 1.55;
    color: var(--text);
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.85rem 1rem;
    white-space: pre;
    overflow-x: auto;
}

.quiz-choices {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}
.quiz-choice {
    display: block;
    width: 100%;
    text-align: left;
    font-family: var(--sans);
    font-size: 0.95rem;
    line-height: 1.55;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.7rem 0.95rem;
    cursor: pointer;
    transition: border-color 0.18s, background 0.18s, color 0.18s;
}
.quiz-choice code {
    font-family: var(--mono);
    font-size: 0.9em;
    background: var(--surface-3);
    padding: 0.02em 0.3em;
    border-radius: 0;
}
.quiz-choice:hover { border-color: var(--amber); }
.quiz-choice[aria-checked="true"],
.quiz-choice.selected {
    border-color: var(--amber);
    background: var(--amber-dim);
    color: var(--text-bright);
}
.quiz-choice[aria-disabled="true"],
.quiz-choice:disabled {
    cursor: default;
    opacity: 0.7;
}
.quiz-choice.correct {
    border-color: var(--accent);
    background: var(--accent-dim);
    color: var(--text-bright);
    opacity: 1;
}
.quiz-choice.wrong {
    border-color: var(--red);
    background: var(--red-dim);
    color: var(--text-bright);
    opacity: 1;
}

.quiz-numeric {
    display: inline-flex;
    align-items: baseline;
    gap: 0.5rem;
}
.quiz-numeric-input {
    font-family: var(--mono);
    font-size: 1rem;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 0;
    padding: 0.5rem 0.75rem;
    width: 9rem;
}
.quiz-numeric-input:focus {
    outline: none;
    border-color: var(--amber);
    box-shadow: 0 0 0 2px var(--amber-glow);
}
.quiz-numeric-unit {
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--text-dim);
}
.quiz-numeric.correct .quiz-numeric-input { border-color: var(--accent); background: var(--accent-dim); }
.quiz-numeric.wrong .quiz-numeric-input { border-color: var(--red); background: var(--red-dim); }

.quiz-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.6rem;
    margin-top: 1.4rem;
}
.quiz-action {
    font-family: var(--mono);
    font-size: 0.85rem;
    letter-spacing: 0.02em;
    padding: 0.6rem 1.2rem;
    border-radius: 0;
    cursor: pointer;
    transition: color 0.18s, background 0.18s, border-color 0.18s;
}
.quiz-action-primary {
    color: var(--surface);
    background: var(--amber);
    border: 1px solid var(--amber);
}
.quiz-action-primary:hover { background: var(--text-bright); border-color: var(--text-bright); }
.quiz-action-primary:disabled {
    cursor: not-allowed;
    opacity: 0.45;
}
.quiz-action-secondary {
    color: var(--text-dim);
    background: var(--surface);
    border: 1px solid var(--border);
}
.quiz-action-secondary:hover { color: var(--text); border-color: var(--text-dim); }
.quiz-action-secondary:disabled {
    cursor: not-allowed;
    opacity: 0.45;
}

.quiz-reveal {
    margin-top: 1.2rem;
    padding: 0 0 0 1rem;
    border-left: 2px solid transparent;
    /* The panel exists in the DOM at all times so aria-live announces
       on content change, not on insertion. Empty state has no visible
       chrome — only the .--correct / .--incorrect modifiers paint a
       border. */
}
.quiz-reveal:empty { padding: 0; border-left-color: transparent; }
.quiz-reveal--correct { border-left-color: var(--accent); }
.quiz-reveal--incorrect { border-left-color: var(--red); }
.quiz-reveal-status {
    font-family: var(--mono);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin: 0 0 0.55rem;
    color: var(--text-dim);
}
.quiz-reveal--correct .quiz-reveal-status { color: var(--accent); }
.quiz-reveal--incorrect .quiz-reveal-status { color: var(--red); }
.quiz-reveal-right {
    font-size: 0.9rem;
    color: var(--text);
    margin: 0 0 0.6rem;
}
.quiz-reveal-explain {
    font-size: 0.92rem;
    line-height: 1.65;
    color: var(--text);
}
.quiz-reveal-explain p + p { margin-top: 0.6rem; }
.quiz-reveal-explain code {
    font-family: var(--mono);
    font-size: 0.9em;
    background: var(--surface-3);
    padding: 0.05em 0.35em;
    border-radius: 0;
}
.quiz-reveal-learn-more {
    font-family: var(--mono);
    font-size: 0.78rem;
    margin-top: 0.7rem;
    color: var(--text-dim);
}
.quiz-reveal-learn-more a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
}
.quiz-reveal-learn-more a:hover {
    border-color: var(--accent);
}

.quiz-results {
    padding: 1.5rem 0.25rem 0.5rem;
    text-align: center;
}
.quiz-results-headline {
    font-family: var(--mono);
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--text-bright);
    margin: 0 0 0.35rem;
    font-variant-numeric: tabular-nums;
}
.quiz-results-sub {
    font-family: var(--mono);
    font-size: 0.82rem;
    color: var(--text-dim);
    margin: 0 0 1.6rem;
}
.quiz-results-newbest {
    color: var(--amber);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.74rem;
}
.quiz-results-misses-heading {
    font-family: var(--mono);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-dim);
    margin: 0 0 0.65rem;
    text-align: left;
}
.quiz-results-misses {
    text-align: left;
    margin-bottom: 1.4rem;
}
.quiz-results-misses td { vertical-align: top; }
.quiz-results-misses a { color: var(--accent); text-decoration: none; border-bottom: 1px dotted var(--border); }
.quiz-results-misses a:hover { border-color: var(--accent); }
.quiz-results-actions { justify-content: center; }
.quiz-restart-btn { background: var(--amber); border-color: var(--amber); color: var(--surface); }
.quiz-restart-btn:hover { background: var(--text-bright); border-color: var(--text-bright); }

/* Narrow viewports — stack the action row full-width so a thumb can
   reach both buttons; settings wrap onto multiple lines. The
   .quiz-best-readout flex auto-margin already drops it below on wrap. */
@media (max-width: 560px) {
    .quiz-actions {
        flex-direction: column;
        align-items: stretch;
    }
    .quiz-action { width: 100%; }
}

/* ──────────────────────────────────────────────────────────────────────
 * CTA BUTTON — a prominent in-page link styled like a primary button.
 * Used for "Try it for yourself →" and other end-of-page calls to action.
 * ────────────────────────────────────────────────────────────────────── */
.cta-button {
    display: inline-block;
    font-family: var(--mono);
    font-size: 0.85rem;
    font-weight: 500;
    letter-spacing: 0.02em;
    padding: 0.7rem 1.4rem;
    background: var(--surface);
    border: 1px solid var(--accent);
    border-radius: 0;
    color: var(--accent);
    text-decoration: none;
    transition: background 0.18s, color 0.18s;
}
.cta-button:hover { background: var(--accent); color: var(--surface); }

/* ──────────────────────────────────────────────────────────────────────
 * THREE-COLUMN PROPERTY-SHEET LAYOUT
 *
 * A denser "workstation-tool" layout for tools where input / output /
 * reference make sense as three distinct surfaces side by side, rather
 * than stacked. Borrows Niagara's property-sheet grammar: label-LEFT /
 * value-RIGHT rows with hairline dividers, small-caps section headers, a
 * recessed reference panel ALONGSIDE the active tool, dense tables. See
 * site-ideas-and-friction.md → "Visual design — two products, one
 * codebase" for the higher-level design philosophy this pattern serves.
 *
 * Adopters: tools/bacnet-ip-converter.html, tools/signal-scaling.html,
 * tools/modbus-register-viewer.html. The PID tuner (tools/pid-tuner.html)
 * deliberately keeps its custom stacked layout — its simulator (canvas +
 * sliders + parameter-style selector + preset chips + metrics) doesn't fit
 * the Input / Output / Reference shape without forcing it; only its
 * symptom → tuning-move cheat sheet adopts the .ref-table-dense styling.
 * ────────────────────────────────────────────────────────────────────── */

/* the grid: three equal columns, butting against the card edges (no gap).
   Each section carries its own padding; the last one is the recessed
   reference panel. Lives directly inside a .tab-pane (or .tool-card), not
   inside a padded .tool-body. */
.tool-body-3col {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
}
.tool-body-3col > section {
    padding: 1.25rem 1.25rem 1.5rem;
    border-right: 1px solid var(--border);
}
.tool-body-3col > section:last-child {
    border-right: none;
    background: var(--surface-3);
}

/* same shape, two equal columns — for tools whose reference content lives
   in a sibling .tool-card rather than the third column (e.g. the BACnet
   converter). No recessed last-child since there is no reference panel. */
.tool-body-2col {
    display: grid;
    grid-template-columns: 1fr 1fr;
}
.tool-body-2col > section {
    padding: 1.25rem 1.25rem 1.5rem;
    border-right: 1px solid var(--border);
}
.tool-body-2col > section:last-child { border-right: none; }

/* Full-width sibling row that sits below a .tool-body-2col grid. Same
   recessed surface + top border as the .tool-body-3col reference column —
   used when a sparse Output column would leave a dense reference / worked-
   example column visually orphaned at the right. No grid template to
   collapse, so no responsive variant needed; the parent (.tab-pane or
   .tool-card) already handles viewport-width changes. */
.tool-body-row {
    padding: 1.25rem 1.25rem 1.5rem;
    border-top: 1px solid var(--border);
    background: var(--surface-3);
}

/* tabs sitting directly above a .tool-body-3col: inset the buttons to line
   up with the column padding, drop the bottom margin so the grid butts
   right up under the tab strip. (The .tab-btn underline indicator is
   unchanged — see the .tabs / .tab-btn rules above.) */
.tabs.tabs-flush { padding: 0 1.25rem; margin-bottom: 0; }

/* small-caps column header inside a 3-col body (INPUT / OUTPUT / …).
   (The redesign mockup called this `.section-label`; renamed to
   .ps-section-label to avoid colliding with the existing page-level
   .section-label used in .section-header.) */
.ps-section-label {
    font-family: var(--sans);
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--text-dim);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    margin: 0 0 0.85rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--border);
}

/* property-sheet row: label on the left, value or input on the right,
   faint hairline between rows. The Niagara-grammar replacement for the
   stacked layout's label-above-input .form-row / .field. */
.ps-row {
    display: grid;
    grid-template-columns: minmax(90px, 38%) 1fr;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid var(--border-faint);
    align-items: center;
    min-height: 38px;
}
.ps-row:last-of-type { border-bottom: none; }
.ps-label {
    font-family: var(--sans);
    font-size: 0.82rem;
    color: var(--text-dim);
    text-transform: none;
    letter-spacing: normal;
}
.ps-value {
    font-family: var(--mono);
    font-size: 0.82rem;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
/* a live / actionable output value. (The mockup called this modifier
   `.readout`; renamed to `.live` because there is already a global
   `.readout` box class — see the bit-viewer / PID-metrics section above —
   that an element with both classes would inherit.) */
.ps-value.live {
    color: var(--blue);
    font-weight: 600;
    font-size: 0.92rem;
    overflow-wrap: anywhere;
}
.ps-value.error { color: var(--red); }  /* out-of-range output (placed after .live so it wins the colour) */
.ps-value.muted {                       /* a note, an absent value, a placeholder */
    color: var(--text-dim);
    font-family: var(--sans);
    font-style: italic;
    font-size: 0.78rem;
}
/* the input variant for a .ps-row's value cell — works on <input>, <select>
   and <textarea>. The element-qualified selectors (not bare `.ps-input`)
   outrank the global `input[type=…]` / `select` block above so the denser
   sizing wins. */
input.ps-input, select.ps-input, textarea.ps-input {
    width: 100%;
    padding: 5px 8px;
    border: 1px solid var(--border);
    background: var(--bg);
    font-family: var(--mono);
    font-size: 0.82rem;
    color: var(--text-bright);
    border-radius: 0;
    -webkit-appearance: none;
}
input.ps-input:focus, select.ps-input:focus, textarea.ps-input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: none;                   /* the dense layout uses a border change, not the glow */
}

/* dense reference table — tighter rows than .ref-table, sans-caps headers,
   for lookup data that lives in a side panel rather than the main flow. */
.ref-table-dense {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.78rem;
}
.ref-table-dense th {
    text-align: left;
    font-family: var(--sans);
    font-size: 0.7rem;
    font-weight: 700;
    color: var(--text-dim);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 6px 8px;
    border-bottom: 1px solid var(--border);
    background: var(--surface-2);
}
.ref-table-dense td {
    padding: 5px 8px;
    border-bottom: 1px solid var(--border-faint);
    font-family: var(--mono);
    color: var(--text);
}
.ref-table-dense td.use {               /* a prose "typical use" cell, not data */
    font-family: var(--sans);
    font-size: 0.78rem;
    color: var(--text-dim);
}
.ref-table-dense tbody tr:nth-child(even) td { background: var(--surface-2); }
.ref-table-dense tbody tr:last-child td { border-bottom: none; }

/* a short explanatory note under a reference table. */
p.ref-note {
    font-family: var(--sans);
    font-size: 0.74rem;
    color: var(--text-dim);
    line-height: 1.55;
    margin-top: 0.85rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--border-faint);
}
/* worked-example pair — .worked-intro paragraph + .worked-list <ol>
   sit together inside a "Worked example" .tool-body-row section. The
   intro paragraph kills the base ref-note's top spacing (the section
   label above already provides it); the list adds its own indent. */
p.ref-note.worked-intro { margin-top: 0; }
ol.ref-note.worked-list { padding-left: 1.2em; }
/* compact ref-note variant for mini-sim captions sitting tight under
   a canvas readout — drops the base rule's border + heavy top spacing. */
p.ref-note.compact {
    margin-top: 0.5rem;
    padding-top: 0;
    border-top: none;
}

/* mobile / responsive — collapse the multi-column grids to a single stack
   (Input → Output → Reference). 3-col collapses earlier (≤1000px) than
   2-col (≤900px): three columns at the new 1120px main cap still leave
   each column ~340px, but at viewport ~1000px (1120px container minus
   side padding minus borders) each column is back to today's cramped
   ~290px. Collapsing 3-col at 1000px keeps psychrometric-chart /
   thermistor-calculator readable on narrow desktops and tablets. 2-col
   tools have headroom and only need to collapse at the tablet break.
   The "two products, one codebase" plan eventually wants a purpose-built
   mobile experience for these tools; this is the conservative pass.
   // mobile-first treatment of this tool is a future task — see site-ideas-and-friction.md */
@media (max-width: 1000px) {
    .tool-body-3col { grid-template-columns: 1fr; }
    .tool-body-3col > section { border-right: none; border-bottom: 1px solid var(--border); }
    .tool-body-3col > section:last-child { border-bottom: none; }
}
@media (max-width: 900px) {
    .tool-body-2col { grid-template-columns: 1fr; }
    .tool-body-2col > section { border-right: none; border-bottom: 1px solid var(--border); }
    .tool-body-2col > section:last-child { border-bottom: none; }
}

/* ──────────────────────────────────────────────────────────────────────
 * EDUCATION-PAGE SVG DIAGRAMS
 *
 * Shared block + legend for hand-written inline schematics on Education
 * pages with pipe-flow diagrams. Consolidated from the per-page
 * .hd-svg / .lp-svg rules once the third page with pipe-flow diagrams
 * (education/pump-control.html) landed — the trigger called out in the
 * friction file's load-piping entry.
 *
 * Supply piping is solid + --blue; return piping is dashed + --blue-cool
 * — distinguishable without colour. Element-level fallbacks (literal
 * hex colours alongside the var()) live in the SVG markup itself, so a
 * stylesheet failure still leaves a legible black-on-white diagram.
 *
 * The flow-active override below: while the flow-engine particle layer
 * is running on an SVG, drop the dashed-return stroke pattern.
 * Direction-of-motion alone distinguishes supply from return at that
 * point, and the stationary dash pattern visually competes with the
 * moving particles. flow-active is added by flow-engine.js only after
 * its reduced-motion bail-out, so reduced-motion keeps the dashes;
 * @media screen scopes the override so a printout still gets the
 * dashed encoding even if the page was animating on screen. Selector
 * intentionally targets any element type (paths and lines both carry
 * data-flow). The .edu-legend return swatch mirrors this: solid by
 * default (matching the animated diagram), dashed again under
 * @media print / prefers-reduced-motion, so the key always agrees
 * with what that visitor actually sees. Page-specific structural
 * diagrams without supply/return (e.g. the VFDs block diagram) stay
 * separate from this family — see vfds.html for the .vfd-svg precedent.
 * ────────────────────────────────────────────────────────────────────── */
.edu-svg {
    display: block;
    width: 100%;
    height: auto;
    margin: 1.5rem 0;
}
.edu-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.5rem;
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin: -0.5rem 0 0;
    padding-left: 0.25rem;
}
.edu-legend .sw { display: inline-block; width: 26px; height: 0; vertical-align: middle; margin-right: 0.4rem; }
.edu-legend .sw.supply { border-top: 3px solid var(--blue); }
.edu-legend .sw.return { border-top: 3px solid var(--blue-cool); }

@media screen {
    svg.flow-active [data-flow="return"] {
        stroke-dasharray: none;
    }
}

@media print, (prefers-reduced-motion: reduce) {
    .edu-legend .sw.return { border-top-style: dashed; }
}

/* ──────────────────────────────────────────────────────────────────────
   WIDGET CHROME — shared "interactive panel" vocabulary used by the
   five widget pages: pump-control (×2 widgets), balancing, vfds,
   vfd-mock, hydronic-loops (d3 twin-T injection-pump widget).

   The widget shell is the recessed --surface-3 panel + the standard
   bits inside it: a "Try this:" prompt-link row, mono caps section
   labels, slider with label/value head, blue-readout pairs, the
   anecdote callout (with min-height to reserve space so reveals
   don't reflow the page below), and the 5-blade fan icon shared
   between the pump-control and vfds widgets. Page-specific
   INTERNALS (LCD, keypad, branch states, valve pills, temperature
   swatches, pump-curve canvas) stay in each page's {% block head %}
   since only that page uses them.

   Default anecdote left rule is --blue (the "extreme-state reward"
   variant on pump-control / vfds / hydronic-loops). The .warn
   modifier flips it to --red for failure callouts (balancing).
*/

.widget {
    margin-top: 1.25rem;
    padding: 1.1rem 1.25rem;
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-radius: 0;
}

.widget-try {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    margin-bottom: 0.85rem;
    line-height: 1.7;
}
.widget-try .lbl { letter-spacing: 0.05em; text-transform: uppercase; margin-right: 0.6rem; }
.widget-try a {
    color: var(--accent);
    text-decoration: none;
    margin-right: 0.85rem;
    cursor: pointer;
    border-bottom: 1px dashed var(--accent-dim);
}
.widget-try a:hover { color: var(--text-bright); border-bottom-color: var(--accent); }

.widget-section-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    margin-bottom: 0.5rem;
}

.widget-slider-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--text-dim);
    margin-bottom: 0.1rem;
}
.widget-slider-head .v {
    color: var(--blue);
    font-weight: 600;
    font-size: 0.95rem;
}
.widget-slider { width: 100%; margin: 0.15rem 0 0.4rem; }

.widget-readout-label {
    font-family: var(--mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    margin-bottom: 0.15rem;
}
.widget-readout-value {
    font-family: var(--mono);
    font-size: 1.0rem;
    font-weight: 600;
    color: var(--blue);
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
}
.widget-readout-value .unit {
    font-size: 0.65em;
    color: var(--text-dim);
    font-weight: 500;
    margin-left: 0.2rem;
}

.widget-anecdote-wrap { min-height: 1.5rem; margin-top: 0.6rem; }
.widget-anecdote {
    background: var(--bg);
    border: 1px solid var(--border);
    border-left: 4px solid var(--blue);
    border-radius: 0;
    padding: 0.75rem 1rem;
    font-size: 0.86rem;
    line-height: 1.7;
    color: var(--text);
}
.widget-anecdote strong { color: var(--text-bright); font-weight: 600; }
.widget-anecdote.warn { border-left-color: var(--red); }

/* ── FAILURE-STATE CALLOUT — sits below a .ps-row result when the input
   branch is invalid (result-value goes muted '—', this callout names
   the failure mode AND the action). Sibling to .widget-anecdote (same
   shell shape, different domain: tool outputs, not education widgets).
   Warm-orange left rule matches the in-use failure-warning hue on
   the .status-pill.warn state below. ── */
.failure-callout {
    margin-top: 0.6rem;
    padding: 0.75rem 1rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-left: 4px solid var(--heat);
    border-radius: 0;
    font-size: 0.86rem;
    line-height: 1.7;
    color: var(--text);
}
.failure-callout strong { color: var(--text-bright); font-weight: 600; }

/* ── STATUS PILL — multi-state verdict chip shown below a tool's result
   rows (feasibility / coil-direction / saturation-pin checks). Mono,
   recessed --surface-2 face; the .ok / .warn / .error modifiers tint the
   border (and the fill, for .ok) per state. Distinct from .failure-callout
   above — that's a single static warn callout with a left rule; this is a
   stateful inline pill. Shared by economizer-ratio, air-mixing,
   coil-sizing, and refrigerant-pt (each formerly carried its own
   page-local copy — .er-feas / .am-status / .cs-status / .rf-status). ── */
.status-pill {
    margin-top: 1rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--rail);
    font-family: var(--mono);
    font-size: 0.82rem;
    line-height: 1.5;
    border: 1px solid var(--border);
    background: var(--surface-2);
    color: var(--text);
}
.status-pill.ok    { border-color: var(--accent); background: var(--accent-dim); }
.status-pill.warn  { border-color: var(--heat);   color: var(--heat); }
.status-pill.error { border-color: var(--red);    color: var(--red); }

/* 5-blade fan icon — visual handle for "the drive is energising
   something." Used by the run/speed widget on vfds and both
   pump-control widgets. The SVG markup (center + 4 ellipses at
   72°/144°/216°/288° around 50,50) lives inline on each page; the
   per-frame rAF loop writes a rotate transform onto the .widget-
   fan-blades group. Pages add their own positioning/sizing
   alongside (e.g. .vfd-w-status-row .widget-fan or .pc-w-fan-wrap
   .widget-fan).

   Reduced-motion: the JS engine short-circuits when the OS pref
   is set, leaving blade angle at 0. The colour swap below still
   communicates running vs stopped. The !important override on
   .widget-fan-blades is belt-and-braces against a stale transform
   attribute lingering on the group. */
.widget-fan ellipse {
    fill: var(--blue-cool);
    transition: fill 0.25s;
}
.widget-fan.spinning ellipse {
    fill: var(--blue);
}

/* ──────────────────────────────────────────────────────────────────────
 * BAS FLARE — "BAS-instrument quiet" widget polish.
 *
 * Subtle panel-instrument hooks reused across widgets:
 *   .bas-led / .bas-led.active / .bas-led.fault — 6px round status
 *     dot. Neutral by default; .active gets the accent fill + a soft
 *     pulsing ring; .fault swaps to --red and pulses faster. aria-
 *     hidden in pages since the adjacent text-state already conveys
 *     status to screen readers.
 *   .lcd-flicker — one-shot 150ms opacity dip; JS adds the class
 *     then removes it on a setTimeout to trigger the dip on every
 *     LCD value update.
 *   .bas-state-flash — one-shot 350ms outer accent-glow ring around
 *     a row/card. JS adds the class on a state transition and
 *     removes it on a setTimeout.
 *   .bas-breathe — slow box-shadow breathing accent ring (token-
 *     driven, reads on both themes). The psychrometric state-point
 *     chip uses a tighter page-local variant; this shared form is
 *     kept for reuse.
 *
 * Reduced motion: the global @media (prefers-reduced-motion: reduce)
 * block below already disables all keyframe animations site-wide, so
 * these rules don't need their own guard. LEDs still color-code state
 * — only the pulse goes quiet.
 * ────────────────────────────────────────────────────────────────────── */
.bas-led {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--text-dim);
    border: 1px solid var(--border);
    vertical-align: middle;
    flex-shrink: 0;
}
.bas-led.active {
    background: var(--accent);
    border-color: var(--accent-glow);
    box-shadow: 0 0 0 2px var(--accent-soft);
    animation: bas-led-pulse 2s ease-in-out infinite;
}
.bas-led.fault {
    background: var(--red);
    border-color: rgba(196,56,47,0.32);
    box-shadow: 0 0 0 2px rgba(196,56,47,0.08);
    animation: bas-led-pulse 1.2s ease-in-out infinite;
}
.bas-led.warn {
    background: var(--heat);
    border-color: rgba(200,120,42,0.32);
    box-shadow: 0 0 0 2px rgba(200,120,42,0.08);
    animation: bas-led-pulse 1.6s ease-in-out infinite;
}
@keyframes bas-led-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

@keyframes lcd-flicker {
    0%   { opacity: 1; }
    45%  { opacity: 0.68; }
    100% { opacity: 1; }
}
.lcd-flicker { animation: lcd-flicker 150ms ease-in-out 1; }

@keyframes bas-state-flash {
    0%   { box-shadow: 0 0 0 2px var(--accent-glow), 0 0 0 4px var(--accent-soft); }
    100% { box-shadow: 0 0 0 2px transparent,        0 0 0 4px transparent; }
}
.bas-state-flash { animation: bas-state-flash 350ms ease-out 1; }

@keyframes bas-breathe {
    0%, 100% { box-shadow: 0 0 0 6px var(--accent-dim); }
    50%      { box-shadow: 0 0 0 11px var(--accent-soft); }
}
.bas-breathe { animation: bas-breathe 1.8s ease-in-out infinite; }

/* ──────────────────────────────────────────────────────────────────────
 * SOFTWARE-REGISTER MOTIFS — Niagara nav-tree + trend sparkline.
 *
 * The software counterparts to the equipment kit: a point-tree (the
 * Niagara/SCADA station cue) and a self-drawing trend line (data-driven
 * motion). Shared so the home-page seam hero and /styleguide.html read
 * from one source. The function-block wiresheet is deliberately NOT
 * promoted here — it has no second consumer yet (codebase-issues#78).
 * ────────────────────────────────────────────────────────────────────── */
.tree { font-family: var(--mono); font-size: 0.78rem; }
.tree .row { display: flex; align-items: center; gap: 0.45rem; padding: 0.28rem 0.4rem; color: var(--text); }
.tree .row.sel { background: var(--blue-dim); color: var(--text-bright); box-shadow: inset 2px 0 0 var(--blue); }
.tree .ic { width: 11px; height: 11px; flex: none; }
.tree .indent { padding-left: 1.2rem; }
.tree .indent2 { padding-left: 2rem; }
.tree .ic.station { background: var(--blue); }
.tree .ic.point { background: var(--accent); }
.tree .ic.alarm { background: var(--red); }
.tree .ic.folder { background: var(--text-dim); }

/* trend sparkline — the line draws itself in once (data-driven motion). A
   consumer that updates the line live (the hero seam) turns the draw-in
   off via its own override. The gradient id `trend-grad` is defined in
   each consuming page's SVG <defs>. */
.trend { width: 100%; height: 54px; display: block; }
.trend .grid-l { stroke: var(--border-faint); stroke-width: 1; }
.trend .area { fill: url(#trend-grad); opacity: 0.4; }
.trend .line {
    fill: none;
    stroke: var(--blue);
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: 600;
    stroke-dashoffset: 600;
    animation: trend-draw 2.2s ease-out forwards;
}
@keyframes trend-draw { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
    .trend .line { stroke-dashoffset: 0; }   /* snap drawn — animation is off */
}

/* ──────────────────────────────────────────────────────────────────────
 * EQUIPMENT REGISTER — warm device face + positive-mode dot-matrix LCD.
 *
 * The second of the two semantic registers (see the :root THEME TOKENS
 * comment). Used ONLY where a page depicts real hardware: VFD/DMM sims,
 * sensor/device widgets, and the one readout in a software tool that
 * shows a physical field value. Warm, dimensional, lit — because it's a
 * device. These tokens (--dev-* / --lcd-*) and look stay CONSTANT across
 * light/dark; a device is a device in either theme.
 *
 * Distilled from design-spike.html (the locked reference). No production
 * page consumes these yet — /styleguide.html is the living consumer, and
 * the VFD mock is the first page slated to adopt them (Phase 3). Keep the
 * register clearly distinct from the flat slate software .panel/.tool-card
 * chrome so the texture *means* "this is hardware."
 * ────────────────────────────────────────────────────────────────────── */

/* Device status LED — glowing, sits on a device face. Distinct from the
   flat software-widget .bas-led: the colour rides a local --c so each
   modifier just reassigns it (no var(--x, #hex) fallback). */
.led {
    --c: var(--accent);
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
    display: inline-block;
    background: var(--c);
    box-shadow: 0 0 5px 0 var(--c);
}
.led--run   { --c: var(--accent); }
.led--alarm { --c: var(--red); animation: blink 1.2s steps(1) infinite; }
.led--comm  { --c: var(--teal); }
.led--warn  { --c: var(--amber); }
.led--off   { --c: #2f3640; box-shadow: inset 0 0 2px rgba(0,0,0,0.7); }
@keyframes blink { 50% { opacity: 0.25; box-shadow: none; } }

/* Device shell — bezel + recessed face. The box-shadow is the physical
   bezel ring + a 2px bottom edge (an equipment dimensional cue, not a
   floating software shadow). */
.device {
    position: relative;
    background: var(--dev-face);
    border: 1px solid var(--dev-edge);
    border-radius: var(--rail);
    padding: 4px;
    box-shadow: 0 0 0 1px var(--dev-bezel), 0 2px 0 rgba(0,0,0,0.5);
}
.device-inner {
    border: 1px solid var(--dev-bezel);
    border-radius: var(--rail);
    padding: 0.7rem;
    background: linear-gradient(180deg, rgba(255,235,180,0.025), transparent 40%), var(--dev-face);
}
.device-tb { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; }
.device-tb .title {
    font-family: var(--mono);
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: #b8b29a;            /* warm gray, legible on the dark device face */
}
.device-tb .badge {
    font-family: var(--mono);
    font-size: 0.56rem;
    color: #6f6a55;
    margin-left: auto;
    letter-spacing: 0.08em;
}

/* The LCD well — lit olive backlight, recessed. The dot-matrix pixel
   mesh (::after, multiply-blended) is the character-LCD tell on BMS /
   VFD keypads. Positive-mode: dark ink on lit field. NO scanlines, NO
   glow — that was the rejected phosphor look. */
.lcd {
    position: relative;
    overflow: hidden;
    background: linear-gradient(180deg, var(--lcd-bg), var(--lcd-bg-2));
    border: 1px solid #20240f;
    border-radius: var(--rail);
    padding: 0.5rem 0.65rem;
    box-shadow: inset 0 0 0 1px rgba(0,0,0,0.3), inset 0 1px 4px rgba(0,0,0,0.4);
}
.lcd::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    mix-blend-mode: multiply;
    background-image: radial-gradient(circle at center, transparent 36%, var(--lcd-grid) 41%);
    background-size: 3px 3px;
}
.lcd .k {
    font-family: var(--mono);
    font-size: 0.54rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: rgba(27,36,16,0.72);
}
.lcd .v {
    font-family: var(--mono);
    font-weight: 700;
    font-size: 1.55rem;
    line-height: 1.05;
    color: var(--lcd-ink);
    margin-top: 0.12rem;
}
.lcd .v small { font-size: 0.66rem; color: rgba(27,36,16,0.6); margin-left: 0.12rem; }

/* Physical keypad — embossed keycaps. Warm key colours are equipment
   constants (the key is plastic, not themed); .go is the run/enter key. */
.keypad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; margin-top: 0.6rem; }
.key {
    font-family: var(--mono);
    font-size: 0.64rem;
    color: #cfc8ad;
    text-align: center;
    padding: 0.5rem 0;
    border-radius: var(--rail);
    background: linear-gradient(180deg, #2a281e, #1b1913);
    border: 1px solid #000;
    box-shadow: inset 0 1px 0 var(--dev-hi);
    cursor: pointer;
}
.key:active { background: #17150f; box-shadow: inset 0 1px 3px #000; }
.key.go { color: #0a1305; background: linear-gradient(180deg, var(--accent-bright), var(--accent)); border-color: #3f6e1d; }

/* Device LED status row — led + caps label clusters under a device. */
.dev-leds { display: flex; gap: 0.9rem; margin-top: 0.6rem; }
.dev-leds span {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-family: var(--mono);
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: #8a8369;
}

/* Bar gauge — used in both registers, tinted per register. Software (.gauge)
   = blue data fill with a small data-glow; equipment (.gauge.eq) = the lit
   olive dot-matrix field with a segmented dark-ink fill. */
.gauge { display: flex; align-items: center; gap: 0.55rem; }
.gauge-label {
    flex: none;
    width: 4em;
    font-family: var(--mono);
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--text-dim);
}
.gauge .track {
    flex: 1;
    height: 10px;
    background: var(--well);
    border: 1px solid var(--border);
    border-radius: var(--rail);
    overflow: hidden;
}
.gauge .fill { height: 100%; width: var(--pct, 60%); background: var(--blue); box-shadow: 0 0 6px var(--blue-glow); }
.gauge .pct { font-family: var(--mono); font-size: 0.72rem; color: var(--text-bright); width: 3.2em; text-align: right; }
.gauge.eq .gauge-label { color: #8a8369; }
.gauge.eq .track {
    position: relative;
    background: linear-gradient(180deg, var(--lcd-bg), var(--lcd-bg-2));
    border-color: #20240f;
}
.gauge.eq .track::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    mix-blend-mode: multiply;
    background-image: radial-gradient(circle at center, transparent 36%, var(--lcd-grid) 41%);
    background-size: 3px 3px;
}
.gauge.eq .fill { background: repeating-linear-gradient(90deg, var(--lcd-ink) 0 4px, rgba(27,36,16,0.22) 4px 7px); box-shadow: none; }
.gauge.eq .pct { color: #c2cf72; }

/* Utility: hide an element that JS toggles into view. display:none
   drops it from layout and the accessibility tree. Placed after the
   component rules so it wins the specificity tie against a
   single-class display rule (e.g. .result-panel's display:flex). */
.hidden { display: none; }

@media (prefers-reduced-motion: reduce) {
    .widget-fan-blades { transform: none !important; }
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}
