/* ──────────────────────────────────────────────────────────────────────
 * 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).
 * ──────────────────────────────────────────────────────────────────── */
/* ── SELF-HOSTED FONTS ──
   Latin woff2 subsets, served from our origin (audit-2026-06 #33,
   owner decision 2026-06-10): the render-blocking Google Fonts CSS
   made FCP hostage to fonts.googleapis.com (measured 624ms → 3.4s
   with a slow fonts origin), and it was the only non-essential
   third-party origin on the site. Both families are OFL-licensed
   (see assets/fonts/LICENSE.txt). Glyph coverage is identical to what
   Google served this site before: no greek/math subset existed, so
   Δ / ≈ / → always came from system fallbacks and still do. The
   unicode-range is Google's latin subset range, kept so fallback
   behavior matches exactly. Overpass ships as one variable file
   covering weights 300–700; IBM Plex Mono as four static instances.
   FONT FILES ARE IMMUTABLE BY NAME — if a font is ever re-subset,
   rename the file (the Worker serves /assets/fonts/* with
   max-age=31536000, immutable). */
@font-face {
    font-family: 'Overpass';
    font-style: normal;
    font-weight: 300 700;
    font-display: swap;
    src: url(/assets/fonts/overpass-latin-var.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url(/assets/fonts/ibm-plex-mono-latin-400.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'IBM Plex Mono';
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url(/assets/fonts/ibm-plex-mono-latin-400-italic.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url(/assets/fonts/ibm-plex-mono-latin-500.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'IBM Plex Mono';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url(/assets/fonts/ibm-plex-mono-latin-600.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

: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-ink:   #86cf4d;   /* accent-colored TEXT that must read at small sizes on the
                                  --accent-dim wash (.tool-tag / .ok-pill) — --accent itself
                                  sits at ~4.1:1 there, an AA small-text fail. The fix is
                                  asymmetric per theme (codebase-issues #166), so it lives in
                                  a purpose token: dark rides --accent-bright's value; light
                                  needs its own deeper green. Dots/borders stay --accent
                                  (3:1 UI floor passes). */
    --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:     #919cab;   /* labels, secondary text — bumped one step 2026-06-10: #828d9b sat at 3.97:1 on --surface-2 where TH cells and nav-card pills carry real content (codebase-issues #81) */
    --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) */
    --red-text:     #f07a6d;   /* alarm TEXT on surfaces — --red reads 3.89:1 on
                                  --surface-2 (AA fail at small sizes); this
                                  lighter step computes 4.91:1. Borders/strokes
                                  keep --red (3:1 UI threshold passes). */
    --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); also outside-air ducts in the education air diagrams */
    --teal-dim:     rgba(70,179,172,0.14);
    --teal-glow:    rgba(70,179,172,0.24);
    --teal-ink:     #5bbfb9;   /* per-section --accent-ink twin (--amber/--plum below too):
                                  the nav-card titlebar pill recolors .ok-pill TEXT via
                                  --section-accent, and the raw hues compute 3.6-4.5:1 on
                                  their -dim washes — an AA small-text fail (codebase-issues
                                  #171, same mechanism as #166). Dots/borders stay on the
                                  raw hue (3:1 UI floor passes). */
    --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);
    --amber-ink:    #e0a94a;   /* = --amber — dark amber already clears AA (4.86:1);
                                  light needs its own deeper step below */
    --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);
    --plum-ink:     #c8a0cf;

    /* 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:       #3a7a14;
    --accent-bright:#428a16;
    --accent-ink:   #356e12;   /* NOT --accent-bright here — brighter WORSENS light-theme
                                  contrast (3.48:1); small accent text needs a deeper green */
    --accent-dim:   rgba(58,122,20,0.10);
    --accent-glow:  rgba(58,122,20,0.22);
    --accent-soft:  rgba(58,122,20,0.06);
    --text:         #38423a;
    --text-bright:  #1d251f;
    --text-dim:     #666e66;
    --blue:         #11679f;
    --blue-bright:  #1573b2;
    --blue-cool:    #4d7286;
    --blue-dim:     rgba(17,103,159,0.10);
    --blue-glow:    rgba(17,103,159,0.22);
    --red:          #c4382f;
    --red-dim:      rgba(196,56,47,0.10);
    --red-text:     #c4382f;   /* light --red already clears AA on light surfaces */
    --heat:         #9c5a14;
    --teal:         #377070;
    --teal-dim:     rgba(55,112,112,0.10);
    --teal-glow:    rgba(55,112,112,0.22);
    --teal-ink:     #346b6b;
    --amber:        #83641f;
    --amber-dim:    rgba(131,100,31,0.10);
    --amber-glow:   rgba(131,100,31,0.22);
    --amber-ink:    #7b5e1d;
    --plum:         #8a5e7e;
    --plum-dim:     rgba(138,94,126,0.10);
    --plum-glow:    rgba(138,94,126,0.22);
    --plum-ink:     #7f5774;
    --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:       #3a7a14;
        --accent-bright:#428a16;
        --accent-ink:   #356e12;
        --accent-dim:   rgba(58,122,20,0.10);
        --accent-glow:  rgba(58,122,20,0.22);
        --accent-soft:  rgba(58,122,20,0.06);
        --text:         #38423a;
        --text-bright:  #1d251f;
        --text-dim:     #666e66;
        --blue:         #11679f;
        --blue-bright:  #1573b2;
        --blue-cool:    #4d7286;
        --blue-dim:     rgba(17,103,159,0.10);
        --blue-glow:    rgba(17,103,159,0.22);
        --red:          #c4382f;
        --red-dim:      rgba(196,56,47,0.10);
        --red-text:     #c4382f;
        --heat:         #9c5a14;
        --teal:         #377070;
        --teal-dim:     rgba(55,112,112,0.10);
        --teal-glow:    rgba(55,112,112,0.22);
        --teal-ink:     #346b6b;
        --amber:        #83641f;
        --amber-dim:    rgba(131,100,31,0.10);
        --amber-glow:   rgba(131,100,31,0.22);
        --amber-ink:    #7b5e1d;
        --plum:         #8a5e7e;
        --plum-dim:     rgba(138,94,126,0.10);
        --plum-glow:    rgba(138,94,126,0.22);
        --plum-ink:     #7f5774;
        --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); }
/* "Start here" newcomer affordance (G-006). A distinct accent-colored entry
   point placed early in the bar so a left-to-right newcomer scan finds the
   on-ramp before the jargon-heavy Tools/Simulators — WITHOUT reordering the
   sections (the experienced-first order is a deliberate v3 decision). Styled
   with color (accent = brand/action) + weight, not a box, so it reads as
   distinct yet inherits the desktop-row and mobile-sheet link layout cleanly.
   Specificity (0,2,1) out-ranks the base .site-nav-links a (0,1,1). */
.site-nav-links a.nav-start-here { color: var(--accent); font-weight: 600; }
.site-nav-links a.nav-start-here:hover { color: var(--accent); 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);
    /* The Tools dropdown crossed a short viewport's height at 18 entries
       (2026-06): cap and scroll internally so the last items stay
       reachable. The mobile sheet overrides this — it already scrolls. */
    max-height: calc(100dvh - 7rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.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); }
/* G-011: a one-line "what's in here" blurb at the top of each dropdown so a
   newcomer can tell from the nav that Simulators/Practice are approachable,
   not advanced — before clicking through to the landing intro. */
.nav-menu-blurb {
    margin: 0;
    padding: 0.55rem 1rem;
    font-family: var(--mono);
    font-size: 0.66rem;
    line-height: 1.45;
    letter-spacing: 0.02em;
    color: var(--text-dim);
    white-space: normal;
    /* Cap the text measure so the blurb wraps to a readable column and
       doesn't drive the panel width. The separating rule lives on the
       next element (below), not this box, so it spans the whole panel
       even when an expanded category widens it past 240px (#140). */
    max-width: 240px;
}
/* Rule under the blurb: carried by the following element (the first
   category group, or the first link in a flat section) so it reaches the
   panel's right edge when an expanded level-2 category — or the mobile
   sheet — widens the panel past the blurb's capped text box. Moving it
   off the blurb is what fixes the short rule (codebase-issues #140). */
.nav-menu-blurb + * { border-top: 1px solid var(--border-faint); }
/* Topic-cluster hub link pinned near the top of the Tools + Education
   dropdowns (rendered by nav-dropdown.njk). Accent-tinted so it reads as
   the marquee entry, above the category rows. */
.nav-menu-featured {
    display: block;
    padding: 0.6rem 1rem;
    border-bottom: 1px solid var(--border);
    font-family: var(--mono);
    font-size: 0.78rem;
    letter-spacing: 0.03em;
    color: var(--accent);
    text-decoration: none;
    background: var(--accent-dim);
    transition: color 0.18s, background 0.18s;
}
.nav-menu-featured:hover { color: var(--text-bright); background: var(--accent-glow); }

/* ── NAV CATEGORY GROUPS (level-2 disclosure) ────────────────────────
   Inside a categorized section's .nav-menu (Tools / Education /
   Practice), pages sit under .nav-group-toggle category rows that expand
   their .nav-submenu (scripts/nav-menu.js). The toggle reads as a quiet
   category header (mono eyebrow, caret right); submenu links indent on a
   faintly inset fill so the nesting is legible. Simulators has no groups
   and is unaffected. */
.nav-menu-group { border-bottom: 1px solid var(--border-faint); }
.nav-menu-group:last-child { border-bottom: none; }
.nav-group-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    width: 100%;
    font-family: var(--mono);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim);
    padding: 0.6rem 1rem;
    background: transparent;
    border: none;
    cursor: pointer;
    transition: color 0.18s, background 0.18s;
}
.nav-group-toggle:hover { color: var(--text); background: var(--accent-dim); }
.nav-group-toggle[aria-expanded="true"] { color: var(--text-bright); }
.nav-group-toggle .nav-caret { flex: none; transition: transform 0.18s; }
.nav-group-toggle[aria-expanded="true"] .nav-caret { transform: rotate(180deg); }
.nav-submenu { display: flex; flex-direction: column; }
.nav-submenu[hidden] { display: none; }
.nav-submenu .nav-menu-item {
    padding-left: 1.75rem;
    border-bottom: none;
    background: var(--surface-2);
}
@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; }
/* The ISO build date would otherwise soft-wrap at its internal
   hyphens at phone widths ("2026-07-" / "08"). */
.console-statusline time { white-space: nowrap; }
.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-ink);
    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-ink);
    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;
}

/* T-011 seeded-demo cue: every calculator ships pre-seeded with a worked
   example that computes a live answer before you touch anything, and the
   seeded inputs ARE the worked example. This one line, between the preamble
   and the inputs, marks the values as demo data so a fast reader doesn't
   mistake the seeded answer for one computed from their own numbers. Mono-dim
   like the preamble; padded to align with the card content inset. */
.ps-seed-note {
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--text-dim);
    padding: 0.6rem 1.5rem 0.2rem;
    margin: 0;
}

/* T-010 collapsible "what this does": the one orienting sentence is the
   always-visible <summary> (so a newcomer is never left without context); the
   rest expands behind "▸ more", keeping the Input/Output grid near the top of
   the card for the repeat expert. Native <details>, mirroring the .pid-spoiler
   ▸/▾ idiom; shares p.tool-preamble's small mono-dim type. Per-tool inline
   padding stays on the <details> (varies by inside/outside .tool-body). */
details.tool-preamble > summary {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    line-height: 1.7;
    cursor: pointer;
    list-style: none;
}
details.tool-preamble > summary::-webkit-details-marker { display: none; }
details.tool-preamble > summary::after {
    content: "▸ more";
    color: var(--accent);
    margin-left: 0.5rem;
    white-space: nowrap;
    font-size: 0.66rem;
    letter-spacing: 0.04em;
}
details.tool-preamble[open] > summary::after { content: "▾ less"; }
details.tool-preamble > summary:hover { color: var(--text); }
details.tool-preamble > .preamble-rest {
    font-family: var(--mono);
    font-size: 0.7rem;
    color: var(--text-dim);
    line-height: 1.7;
    margin-top: 0.5rem;
}

/* First-use acronym gloss (G-008). A quiet dotted underline cues "there's
   a definition here"; the title shows the expansion on desktop hover. The
   hover is a BONUS, not load-bearing — touch devices have no hover, so a
   genuinely newcomer-critical term gets a parenthetical in the prose
   instead; <abbr> carries the broader incidental acronym load. Theme-aware
   (the underline tracks --text-dim). cursor:help signals the hover. */
abbr[title] {
    text-decoration: underline dotted var(--text-dim);
    text-underline-offset: 0.18em;
    cursor: help;
}

/* 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); }

/* ── TOPIC-HUB LANDINGS ── */
/* Group / sub-group headings and the numbered "start here" reading-path
   line for the pillar-hub landings (/bacnet/, /forced-air/, and future
   topic clusters). Promoted here from the BACnet hub's page-local block
   once a second hub landing needed the same chrome. Heading scale
   mirrors practice/index.html's .practice-section-heading. */
.hub-group {
    font-family: var(--mono);
    font-size: 0.95rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-bright);
    margin: 2.4rem 0 0.35rem;
}
.hub-group-intro {
    font-size: 0.94rem;
    line-height: 1.7;
    color: var(--text-dim);
    margin: 0 0 1.1rem;
    max-width: 56rem;
}
.hub-sub {
    font-family: var(--mono);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--accent);
    margin: 1.5rem 0 0.65rem;
}
.hub-path {
    font-family: var(--mono);
    font-size: 0.9rem;
    line-height: 1.95;
    margin: 0 0 1.4rem;
}
.hub-path a { color: var(--accent); text-decoration: none; }
.hub-path a:hover { color: var(--text-bright); }
.hub-path .hub-step { color: var(--text-dim); }
.hub-path .hub-arrow { color: var(--text-dim); margin: 0 0.35rem; }

/* ── TABS ── */
/* flex-wrap + nowrap buttons: at phone widths whole buttons wrap onto a
   second row instead of the row clipping inside the overflow-hidden
   .tool-card (4 tabs on electrical-quick-calc) or the labels soft-
   wrapping into 2–3-line buttons ("HEX →" / "IP"). Inert wherever the
   row fits. */
.tabs {
    display: flex;
    flex-wrap: wrap;
    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;
    white-space: nowrap;
    padding: 0.55rem 1.1rem;
    border: none;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: color 0.18s, border-color 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: background 0.12s, border-color 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;                   /* rows carry their own separators + padding */
        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;
        /* The sheet itself scrolls — an inner cap here would double-scroll. */
        max-height: none;
        overflow-y: visible;
    }
    /* 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; }
    /* Sheet reads as a tidy list, not floating text. Every top-level row is
       full-width with a hairline separator; dropdown rows push their caret
       to the right edge and make the label a full-width tap target (tap
       label → landing, tap caret → expand); open sub-menus indent under
       their parent. The .site-nav.nav-open prefix out-ranks the touch-floor
       block, so these hold whether or not the device reports hover:none. */
    .site-nav.nav-open .site-nav-links > a,
    .site-nav.nav-open .site-nav-links > .nav-item--has-menu {
        border-bottom: 1px solid var(--border-faint);
    }
    .site-nav.nav-open .site-nav-links > a { padding: 0.6rem 0.25rem; }
    .site-nav.nav-open .nav-item--has-menu { justify-content: space-between; }
    .site-nav.nav-open .nav-item--has-menu > a { flex: 1 1 auto; padding: 0.6rem 0.25rem; }
    .site-nav.nav-open .nav-item--has-menu > .nav-menu-toggle { margin: 0; padding: 0.6rem 0.6rem; }
    .site-nav.nav-open .nav-menu { margin-top: 0; }
    .site-nav.nav-open .nav-menu .nav-menu-item { padding-left: 1.25rem; }
    /* Level-2 in the sheet: category rows are full-width with the caret at
       the right edge (a tap target the width of the row); their pages
       indent a second step so the hierarchy reads. */
    .site-nav.nav-open .nav-group-toggle { padding: 0.6rem 0.25rem; }
    .site-nav.nav-open .nav-submenu .nav-menu-item { padding-left: 2.25rem; }
    .site-nav.nav-open .units-toggle,
    .site-nav.nav-open .theme-toggle { padding: 0.5rem 0.25rem; }
}
/* ── 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: color 0.15s, border-color 0.15s, background 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 OR on a touch-primary device (e.g. drag-wiring
   editors — same gate those pages use to swap the sim for a tips panel). */
.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), (hover: none) and (pointer: coarse) {
    .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-start-here:focus-visible,
.nav-hamburger:focus-visible,
.nav-menu-toggle:focus-visible,
.nav-group-toggle:focus-visible,
.nav-menu-item:focus-visible,
.nav-menu-featured: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,
.faq-a a:focus-visible,
.lesson-seq-link: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,
.quiz-next-link:focus-visible,
.bit:focus-visible,
.psy-pill:focus-visible,
#th-rt-scroll:focus-visible,
#fbe-canvas:focus-visible,
.bo-id.bo-copyable:focus-visible,
.bvid-copyable:focus-visible,
.bec-id.bec-copyable:focus-visible,
.bpri-val:focus-visible,
.bpri-release:focus-visible,
.widget-try button:focus-visible,
.ah-w-mode-btn:focus-visible,
.aid-w-mode-btn:focus-visible,
.bp-w-mode-btn:focus-visible,
.ds-w-mode-btn:focus-visible,
.vav-w-mode-btn:focus-visible,
.bec-mode:focus-visible,
details.tool-preamble > summary: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 .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), the fullscreen
   button (26px), and the form-control family (#164 measured
   input.ps-input AND select.ps-input at 29px and .field selects at
   38.6px — this block's old ".ps-input fields clear 44px natively"
   claim was equally false; #172 caught the .field INPUTS the #164
   pass missed, at 38.6px). Like FOCUS INDICATORS above, this is
   the single consolidated home for the floor — add new interactives
   here rather than scattering per-component min-heights. Shared-class
   selectors only: widget INTERNALS with no shared class (e.g. the
   vfds source selects) get a page-local (hover: none) floor in their
   own head block instead.
   See codebase-issues #74 / #164 / #172 / 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,
    .bec-mode,
    .tool-card-fullscreen-btn,
    .quiz-action,
    .quiz-reset-best {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 44px;
    }
    /* The display:inline-flex above beats the UA-default
       `[hidden] { display: none }` — the same trap .nav-card[hidden]
       and the .rt-stack tr[hidden] re-assert against elsewhere.
       Re-assert it here, or the refrigerant-loop sim's mode-filtered
       preset row leaks BOTH modes' scenario buttons on touch. */
    .copy-btn[hidden] { display: none; }
    /* 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; }
    /* Category toggles get min-height ONLY — they're already
       display:flex with the label left + caret right (the shared center
       declaration above would override that justify-content). */
    .nav-group-toggle { min-height: 44px; }
    /* flex-start (not the group's center above): on the desktop bar the
       links are content-width so this is moot, but in the full-width mobile
       sheet the stretched links would otherwise center. Left-align them to
       match the dropdown rows. */
    .site-nav-links a { padding-inline: 0.2rem; justify-content: flex-start; }
    .quiz-settings-select { min-height: 44px; }
    /* Form-control family (#164 / #172): min-height ONLY — replaced
       elements grow their box directly and vertically center their
       text; the group's inline-flex display above would fight native
       select / input rendering. Floored together per the 2026-07-18
       owner call: flooring only the selects would leave 44px selects
       towering over 29px inputs in the same property sheet. #172
       added .field input — #164 floored the .field selects but never
       the inputs, which sat at 38.6px on contact / pid-tuner /
       staging-sequencer. The contact honeypot is .hp-field, not
       .field, so the floor can't catch it, and `input` never matches
       the textareas — contact's message box and textarea.ps-input
       (duct-traverse readings) are multi-row and clear the floor
       natively. */
    .field input,
    .field select,
    input.ps-input,
    select.ps-input { min-height: 44px; }
    /* Disclosure summaries get min-height ONLY — they're left-aligned text
       lines, so the group's center declaration would mis-align them. */
    .pid-spoiler > summary,
    details.tool-preamble > 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: pid-tuner.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;
}

/* ── DESKTOP-ONLY SIM FALLBACK — shown in place of a drag-wiring editor
   (controller-wiring, function-block-editor) on a screen that can't run
   it: below the 1000px / .fs-desktop-only line, OR any touch-primary
   device (a finger can't both drag a block and pan the canvas, so these
   are pointer-only by design). The page hides its live workspace and
   flips this panel visible in the SAME media query — the gate lives on
   the page, this is just the shared shell. Carries a "come back on a
   desktop" line plus a few field tips so the visit still teaches. Hidden
   by default; pages opt in per the .narrow-width-note pattern. Voice:
   orientation, not apology. ── */
.desktop-only-sim {
    display: none;
    background: var(--surface-3);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 0;
    padding: 0.95rem 1.15rem 1.05rem;
    margin: 1rem 0 1.25rem;
}
.desktop-only-sim .dos-label {
    font-family: var(--mono);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--accent);
    margin: 0 0 0.5rem;
}
.desktop-only-sim p {
    font-size: 0.9rem;
    line-height: 1.65;
    color: var(--text);
    margin: 0 0 0.75rem;
}
.desktop-only-sim ul { list-style: none; margin: 0; padding: 0; }
.desktop-only-sim li {
    position: relative;
    font-size: 0.88rem;
    line-height: 1.6;
    color: var(--text);
    padding: 0.25rem 0 0.25rem 1.1rem;
}
.desktop-only-sim li::before {
    content: "▸";
    position: absolute;
    left: 0;
    color: var(--accent);
}
.desktop-only-sim strong { color: var(--text-bright); }
.desktop-only-sim .dos-more { margin: 0.85rem 0 0; font-size: 0.86rem; }

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). The -ink twin
   carries the pill's TEXT (see the .ok-pill note below); it defaults
   to --accent-ink, so the green sections need no override. 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);
    --section-accent-ink:  var(--accent-ink);
    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;
}
/* Featured / marquee card — spans the full grid row. Applied via the
   navCard() `wide` param (e.g. the BACnet hub atop the home Browse grid). */
.nav-card--wide { grid-column: 1 / -1; }
.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);  --section-accent-ink: var(--plum-ink); }
.nav-card--simulators { --section-accent: var(--teal);   --section-accent-dim: var(--teal-dim);   --section-accent-glow: var(--teal-glow);  --section-accent-ink: var(--teal-ink); }
.nav-card--practice   { --section-accent: var(--amber);  --section-accent-dim: var(--amber-dim);  --section-accent-glow: var(--amber-glow); --section-accent-ink: var(--amber-ink); }
.nav-card--home       { --section-accent: var(--accent); --section-accent-dim: var(--accent-dim); --section-accent-glow: var(--accent-glow); }
.nav-card--reference  { --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.
       TEXT rides the -ink twin — the raw section hues compute
       3.6-4.5:1 on their -dim washes at this 0.62rem size, an AA
       small-text fail in 6 of 8 accent x theme combos (codebase-issues
       #171; same mechanism as --accent-ink, #166). The dot + border
       stay on the raw hue (the 3:1 UI floor passes everywhere). */
    background: var(--section-accent-dim);
    color: var(--section-accent-ink);
    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 {
    /* inline-block: each pill is an atomic inline, so the statusline
       wraps between pills instead of mid-pill ("… · 6" / "SIMULATORS",
       a stranded "~" from "~ 5 MIN"). */
    display: inline-block;
}
.nav-card-pill:not(:last-child)::after {
    content: '•';
    margin: 0 0.35rem;
    color: var(--border);
}
/* Platform marker (navCard desktopOnly) — amber + bold so it reads as a
   constraint, not another feature pill. The page gates its canvas on
   touch (the .desktop-only-sim swap); this warns before the tap-in. */
.nav-card-pill--desktop { color: var(--amber); font-weight: 600; }

.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;
    font-size: 0.92rem;   /* pin: this is a decorative "Active" status, not a tool result — opt out of the T-009 .ps-value.live size lift */
}
.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); }

/* ──────────────────────────────────────────────────────────────────────
 * LESSON SEQUENCE PAGER (E-016) — visible prev/next on curriculum lessons.
 * Rendered in layouts/page.njk from educationSequence.js (the same source
 * head.njk uses for rel=prev/next). Sits below the page's back-link as a
 * continue-the-series bar; width + horizontal padding match `main`.
 * ────────────────────────────────────────────────────────────────────── */
.lesson-seq {
    max-width: 1120px;
    width: 100%;
    margin: 0 auto;
    padding: 0 2rem 3.5rem;
    display: flex;
    justify-content: space-between;
    gap: 1rem;
}
.lesson-seq-col { display: flex; flex: 1 1 0; min-width: 0; }
.lesson-seq-col--next { justify-content: flex-end; }
.lesson-seq-link {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    width: 100%;
    min-width: 0;
    padding: 0.7rem 1rem;
    border: 1px solid var(--border);
    background: var(--surface);
    text-decoration: none;
    transition: border-color 0.12s;
}
.lesson-seq-link:hover { border-color: var(--accent); }
.lesson-seq-next { align-items: flex-end; text-align: right; }
.lesson-seq-dir {
    font-family: var(--mono);
    font-size: 0.62rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
}
.lesson-seq-title {
    font-size: 0.9rem;
    color: var(--accent);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
@media (max-width: 620px) {
    .lesson-seq { flex-direction: column; padding-left: 1rem; padding-right: 1rem; }
    .lesson-seq-next { align-items: flex-start; text-align: left; }
}

/* ──────────────────────────────────────────────────────────────────────
 * 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);
}

/* ──────────────────────────────────────────────────────────────────────
 * FAQ BLOCK — end-of-page hand-written Q&A on tool pages. Rendered by
 * html/_includes/faq.njk from the page's `faqs:` frontmatter, which also
 * feeds the FAQPage JSON-LD (faqJsonLd filter) — one source, so the
 * on-page copy and the structured data can't drift. Questions are <h3>
 * so the searched phrase lands in a heading (an on-page ranking lever).
 * Sits inside <main> near the end, above relatedLinks / .back-link.
 * ────────────────────────────────────────────────────────────────────── */
.faq-block {
    margin-top: 2rem;
}
.faq-q {
    font-family: var(--sans);
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text-bright);
    line-height: 1.4;
    margin: 1.3rem 0 0.35rem;
}
.faq-q:first-of-type {
    margin-top: 0.9rem;
}
.faq-a {
    font-family: var(--sans);
    font-size: 0.85rem;
    color: var(--text-dim);
    line-height: 1.6;
    margin: 0;
}
.faq-a a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px dotted var(--border);
}
.faq-a a:hover {
    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;
}
/* Results-card "Next quiz →" link — an <a> wearing the action chrome
   (secondary flavor), so anchor defaults need resetting. */
.quiz-next-link {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    color: var(--accent);
    background: var(--surface);
    border: 1px solid var(--accent);
}
.quiz-next-link:hover { background: var(--accent-dim); color: var(--text-bright); }
.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;
}
/* A perfect run that didn't beat a longer stored best still gets a green
   'flawless' nod (P-006). Distinct hue from the amber new-best so the two
   acknowledgements read as different things. */
.quiz-results-flawless {
    color: var(--accent);
    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: 1.1rem;       /* T-009: lifted from 0.92rem so the answer outweighs the 0.82rem label — size, not just colour, now carries the hierarchy */
    overflow-wrap: anywhere;
}
/* T-009 primary headline: the single solved-for value a tool exists to produce
   (signal-scaling output, coil total capacity, valve Cv …). The marker sits on
   the STATIC .ps-row — setLive() rewrites the value element's className
   wholesale (`el.className = 'ps-value live'`), so a modifier on the value would
   be wiped; the row is never touched by the result JS. */
.ps-row.primary .ps-value.live { font-size: 1.4rem; }
.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; }

/* stacked-row opt-in (.rt-stack) for lookup tables whose last column is
   prose: below 620px each row stacks — the leading cells (id, name)
   share a header line and the prose cell drops to a full-width line
   beneath — so nothing clips inside the overflow-hidden .tool-card and
   nothing needs a sideways scroll. Works on .ref-table and
   .ref-table-dense alike; pair with a .table-scroll wrapper as an
   inert safety net. Mechanics that are load-bearing here:
   - tr[hidden] must be restated — the author display:flex beats the UA
     [hidden] rule, else the bacnet-objects filter stops hiding rows;
   - the cell rules ride the two-class tbody-tr-td shape (0,2,3) to
     out-rank page-local column classes (.bo-name's nowrap is (0,2,0));
   - zebra moves from the cells to the row (the dense base paints td
     backgrounds — killed by the later background:none at equal
     specificity). */
@media (max-width: 620px) {
    .rt-stack thead { display: none; }
    .rt-stack tbody tr {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 0 0.6rem;
        padding: 0.5rem 0.5rem;
        border-bottom: 1px solid var(--border-faint);
    }
    .rt-stack tbody tr[hidden] { display: none; }
    .rt-stack tbody tr:last-child { border-bottom: none; }
    .ref-table.rt-stack tbody tr td,
    .ref-table-dense.rt-stack tbody tr td {
        display: block;
        padding: 0;
        border-bottom: none;
        background: none;
        white-space: normal;
    }
    .ref-table.rt-stack tbody tr td:last-child,
    .ref-table-dense.rt-stack tbody tr td:last-child {
        flex-basis: 100%;
        font-family: var(--sans);
        font-size: 0.78rem;
        line-height: 1.5;
        color: var(--text-dim);
        margin-top: 0.2rem;
    }
    .rt-stack tbody tr:nth-child(even) { background: var(--surface-2); }
}

/* 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; }
    /* min-width: 0 kills the grid item's automatic minimum — without it
       a 1fr track can't shrink below the section's min-content, so a
       wide table's width punches through its .table-scroll wrapper and
       the card clips it (the wrapper only scrolls when the section is
       allowed to be narrower than the table). */
    .tool-body-3col > section { border-right: none; border-bottom: 1px solid var(--border); min-width: 0; }
    .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); min-width: 0; }
    .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.
 *
 * Air ducts (education/air-handlers.html onward) ride the same family
 * with data-flow="air": particle fill follows each duct's stroke
 * attribute (OA --teal, RA --blue-cool, SA --blue, EA --text-dim), so
 * there is no per-stream engine type. Exhaust/relief ducts are dashed
 * — the non-color encoding, mirroring the water return — and drop
 * their dashes while animated exactly like return piping. The .sw.oa /
 * .sw.ra / .sw.sa / .sw.ea legend swatches below follow the same
 * solid-while-animated / dashed-when-static agreement as .sw.return.
 * ────────────────────────────────────────────────────────────────────── */
.edu-svg {
    display: block;
    width: 100%;
    height: auto;
    margin: 1.5rem 0;
}
/* Flow-main directional captions on the pipe-flow diagrams ("supply
   main →" / "← return main"). Promoted from per-<text> presentation
   attributes that carried the same shape on hydronic-loops /
   load-piping / pump-control (codebase-issues #141): 11px mono, normal
   weight, var(--text) — inherited from the parent svg's mono. Only the
   font/fill move here; the positional attrs (x / y / text-anchor) stay
   inline since they vary per caption. The smaller inset-diagram pair
   (10px / var(--text-dim)) is a deliberately quieter treatment and
   keeps its own inline attrs. */
.edu-caption {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 400;
    fill: var(--text);
}
.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); }
.edu-legend .sw.oa { border-top: 3px solid var(--teal); }
.edu-legend .sw.ra { border-top: 3px solid var(--blue-cool); }
.edu-legend .sw.sa { border-top: 3px solid var(--blue); }
.edu-legend .sw.ea { border-top: 3px solid var(--text-dim); }

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

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

/* ──────────────────────────────────────────────────────────────────────
   BLOCK-AND-BYTE DIAGRAMS — static labeled-box schematics on the
   protocol Education pages (.mb-svg: Modbus Basics / Decoding;
   .bac-svg: BACnet Basics / Networking / MS/TP). Consolidated from
   per-page {% block head %} copies when the third BACnet-family page
   landed (the trigger both families recorded). Distinct from the
   pipe-flow .edu-svg family (no data-flow, no animation) and from
   the page-local .fb-svg / .vfd-svg structural diagrams (single
   consumers). The four per-page copies had drifted to a mix of 740px
   and 760px on .wide — unified here at 760 (the variance was copy
   drift, not intent; a 20px max-width delta is invisible in practice).
   ────────────────────────────────────────────────────────────────────── */
.bac-svg,
.mb-svg {
    display: block;
    width: 100%;
    height: auto;
    max-width: 620px;
    margin: 1.4rem auto 0.6rem;
}
.bac-svg.wide,
.mb-svg.wide { max-width: 760px; }

/* ──────────────────────────────────────────────────────────────────────
   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); }
/* Button twin of the anchor row — same look, real <button> semantics
   for keyboard/AT reach (the anchors above predate the #142/#143
   audit finding; new try-rows should use buttons). */
.widget-try button {
    font: inherit;
    color: var(--accent);
    background: none;
    border: none;
    border-bottom: 1px dashed var(--accent-dim);
    padding: 0;
    margin-right: 0.85rem;
    cursor: pointer;
}
.widget-try button: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);
   dew-point-calculator rides the base with page-local deltas
   (.dew-verdict — pad, .ok brightness, amber .edge state). ── */
.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-text); }

/* 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;
    /* 0.85 (was 0.72): the caption computed 3.69:1 against the darker
       backlight-gradient stop — AA fail at this size. 0.85 → 4.75:1. */
    color: rgba(27,36,16,0.85);
}
.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.85); margin-left: 0.12rem; }   /* 0.85 (was 0.6, a 2.91:1 AA fail) — see .lcd .k */

/* 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;
    }
}
