/* DI Ware — Deadly Input brand theme.
   Black & white per the brand guide, plus the two functional accents real DI hardware uses: a
   green status LED and an amber action color. Red Hat type throughout; the wordmark is Range
   Unique, whose D and I ARE the logo's letterforms. Product strings live in js/product.js. */

@font-face{font-family:'Red Hat Display';src:url('../assets/fonts/RedHatDisplay-Black.woff2') format('woff2');font-weight:900;font-display:swap}
@font-face{font-family:'Red Hat Display';src:url('../assets/fonts/RedHatDisplay-Bold.woff2') format('woff2');font-weight:700;font-display:swap}
@font-face{font-family:'Red Hat Display';src:url('../assets/fonts/RedHatDisplay-Medium.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Red Hat Text';src:url('../assets/fonts/RedHatText-Regular.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Red Hat Text';src:url('../assets/fonts/RedHatText-Medium.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Red Hat Mono';src:url('../assets/fonts/RedHatMono-Regular.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Range Unique';src:url('../assets/fonts/RangeUniqueDemo.otf') format('opentype');font-weight:400;font-display:swap}

:root{
  /* Tell the browser this UI is dark, so the parts it draws ITSELF follow — select popups,
     scrollbars, focus rings, the default form widgets. Without it those render in the light scheme
     against a black app, which is how the button-assignment dropdowns ended up white-on-white:
     .ms-assign sets `background:none`, so its popup kept the light default while the options
     inherited --ink. That was reported from the bench as unreadable until moused over. The explicit
     option rule further down is the belt to this braces — a popup's colors should not depend on
     the OS honoring a hint. */
  color-scheme: dark;
  --bg:#000; --panel:#0c0c0d; --panel2:#151517; --panel3:#1c1c1f; --line:#262629; --line2:#333338;
  --ink:#fff; --muted:#8b8b90; --muted2:#5f5f65; --accent:#fff;
  --fill-a:#fff; --fill-b:#9a9aa0; --ok:#37d178; --warn:#f2a900; --err:#ff5470; --lift:#6cb2ff;
  --key:#161619; --key-line:#303036;

  /* ---- depth, not blur ----------------------------------------------------------------------
     Frosted translucency needs something colorful behind it to refract. The brand is Pantone
     Process Black and white, so on this page glass reads as gray haze over gray haze — the
     hierarchy has to come from elevation, radius and hairlines instead. Those are on fixed
     scales here because being ad-hoc is most of why the app read as unstyled: radii of
     4/5/6/8/9/10/11/12/14/24 px were all in use at once, and no two paddings agreed. */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:22px; --sp-6:30px;
  --r-1:6px; --r-2:9px; --r-3:12px; --r-4:16px; --r-pill:999px;
  /* A hairline of light along the top edge is what makes a near-black panel read as RAISED
     rather than as a hole cut in the page — on a dark UI it does the work a drop shadow does on
     a light one, and the shadow underneath only has to keep the edge from floating. */
  --e-1:0 1px 0 rgba(255,255,255,.035) inset, 0 10px 26px -18px rgba(0,0,0,.95);
  --e-2:0 1px 0 rgba(255,255,255,.05) inset, 0 20px 50px -22px rgba(0,0,0,.95);
  --e-lift:0 2px 6px -1px rgba(0,0,0,.55);
  /* Two rings, black then white, so focus is visible against both the page and a white button. */
  --ring:0 0 0 2px #000, 0 0 0 4px rgba(255,255,255,.7);
  --dur:.14s;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font-family:'Red Hat Text',system-ui,sans-serif;
  font-size:14px;-webkit-font-smoothing:antialiased;display:flex;flex-direction:column}
h1,h2,h3,h4{font-family:'Red Hat Display',sans-serif;margin:0}

/* header */
header{display:flex;align-items:center;gap:14px;padding:10px 20px;border-bottom:1px solid var(--line);
  background:var(--panel);flex:none}
/* HEADER-UNUSED since 2026-09-01 -- the roundel was taken out of the header; the splash keeps its
   own .sp-mark. Kept because "put the mark back" is a plausible ask and this is the tuned size. */
.di-mark{width:32px;height:32px;border-radius:50%;background:#fff;display:grid;place-items:center;flex:none}
.di-mark img{width:21px;height:21px;display:block}
.wordmark{font-family:'Range Unique','Red Hat Display',sans-serif;font-size:19px;letter-spacing:1px;line-height:.9;color:#fff}
/* margin-top: the wordmark runs at line-height .9 and Range Unique has almost no descender room,
   so with the roundel gone (it used to set the block's height) the two lines sat on top of each
   other. 5px is the gap that reads as "subtitle of" rather than "second line of". */
.tagline{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted);letter-spacing:2px;
  text-transform:uppercase;margin-top:5px}
header .spacer,.scopebar .spacer{flex:1}
#devinfo{color:var(--muted);font-family:'Red Hat Mono',monospace;font-size:12px}
#status{font-family:'Red Hat Mono',monospace;font-size:12px;display:flex;align-items:center;gap:6px}
#status::before{content:'';width:8px;height:8px;border-radius:50%;background:var(--muted2)}
#status.ok::before{background:var(--ok);box-shadow:0 0 8px var(--ok)}
#status.err::before{background:var(--err)} #status.warn::before{background:var(--warn)}
/* Two button weights and nothing else. `.primary` is an alias for the bare element rather than a
   separate rule, because half the buttons in the app carried `style="background:#fff;color:#000"`
   to say "this one is the primary" — which is what the default already was. */
button,button.primary{background:#fff;color:#000;border:1px solid transparent;border-radius:var(--r-1);
  padding:9px 16px;font-family:'Red Hat Display';font-weight:700;font-size:13px;cursor:pointer;
  box-shadow:var(--e-lift);
  transition:background var(--dur),border-color var(--dur),color var(--dur),
             transform var(--dur),box-shadow var(--dur)}
button:hover,button.primary:hover{background:#e8e8ea;filter:none}
button.ghost{background:var(--panel2);color:var(--ink);border-color:var(--line2);box-shadow:none}
button.ghost:hover{background:var(--panel3);border-color:#45454d;filter:none}
button:active{transform:translateY(1px);box-shadow:none}
button:disabled{opacity:.38;cursor:not-allowed;box-shadow:none;transform:none}
button:disabled:hover{background:#fff} button.ghost:disabled:hover{background:var(--panel2)}
/* One focus treatment for everything focusable. :focus-visible only, so a mouse click on a
   button doesn't leave a ring sitting on it. */
:where(button,a[data-page],a[data-go],input,select,textarea,[tabindex]):focus-visible{
  outline:none;box-shadow:var(--ring)}
/* A row of buttons. This replaces `<div class="pill" style="background:none;padding:0">`, which
   appeared eight times: a segmented control with its own look switched off to stand in for the
   plain button row that didn't exist. */
.btnrow{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap}
.ok{color:var(--ok)}.err{color:var(--err)}.warn{color:var(--warn)}.muted{color:var(--muted)}
.dim{opacity:.6}
/* Any author `display` rule outranks the browser's [hidden] default, so state this loudly
   once — otherwise every hidden panel with a display of its own renders anyway. */
[hidden],.hidden{display:none!important}
label.switch{display:flex;align-items:center;gap:6px;color:var(--muted);cursor:pointer;font-family:'Red Hat Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:.5px}

/* shell: sidebar + content */
.shell{display:flex;flex:1;min-height:0}
.sidebar{width:232px;flex:none;background:var(--panel);border-right:1px solid var(--line);
  padding:var(--sp-3) var(--sp-2);overflow:auto}
.nav-group{font-family:'Red Hat Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted2);margin:16px 10px 6px}
.nav-group:first-child{margin-top:4px}
.nav-item{display:flex;align-items:center;gap:11px;padding:9px 11px;border-radius:var(--r-2);color:var(--muted);cursor:pointer;text-decoration:none;font-weight:500;font-size:13.5px;
  transition:background var(--dur),color var(--dur)}
/* The icon set (js/icons.js) is stroke-only and draws in currentColor, which is what lets the
   active item's white-on-black inversion work with no per-state icon rule. The box is fixed at
   18px so labels line up down the column. Icons sit a little quieter than their label at rest,
   so the sidebar reads as text with marks beside it rather than a column of drawings. */
.nav-item .ni{width:18px;height:18px;flex:none;display:grid;place-items:center;opacity:.72;transition:opacity .12s}
.nav-item .ni .icn{display:block}
.nav-item:hover .ni,.nav-item.on .ni{opacity:1}
.nav-item .nl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nav-item:hover{background:var(--panel3);color:var(--ink)}
.nav-item.on{background:#fff;color:#000;font-weight:700;box-shadow:var(--e-lift)}
/* Nudged UP on the active item, which is the counter-intuitive direction: a light stroke on a dark
   field blooms and reads heavier than the same width dark-on-light, so 1.6px that looks right in
   the resting sidebar looks thin once the item inverts — especially beside a label that also goes
   bold. Matching the perceived weight takes slightly more ink, not less. */
.nav-item.on .ni .icn{stroke-width:1.75}
/* Still being read off the board. Dimmed but clickable — unlike .disabled — and marked with a
   pulsing dot after the label so it reads as "not yet" rather than "not available". Placed after
   .on so a loading page that is also the current page keeps the dot. */
.nav-item.loading{opacity:.55}
.nav-item.loading::after{content:'';width:5px;height:5px;border-radius:50%;background:var(--accent);
  margin-left:auto;flex:none;animation:navpulse 1.1s ease-in-out infinite}
@keyframes navpulse{0%,100%{opacity:.25}50%{opacity:1}}
/* Parked page (Calibration): visible so people know it's coming, not clickable. */
.nav-item.disabled{opacity:.4;cursor:default;pointer-events:none}
.nav-item.disabled:hover{background:none;color:var(--muted)}
.nav-item .nav-soon{margin-left:auto;flex:none;font-size:10px;font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted2);border:1px solid var(--line2);border-radius:var(--r-pill);
  padding:1px 7px}
.content{flex:1;min-width:0;overflow:auto;padding:var(--sp-4) var(--sp-5) 90px;
  display:flex;flex-direction:column;gap:var(--sp-4)}
/* #page had no rule at all, so a page with two blocks stacked them edge to edge and their 1px
   borders drew one doubled line — two sections reading as a single mis-drawn box. */
#page{display:flex;flex-direction:column;gap:var(--sp-4)}

/* ---- the section: one page primitive ------------------------------------------------------
   Every page used to hand-roll `<div class="card">`, and 7 of the 23 carried a heading. So the
   same kind of block looked different on every page, and nothing said what a block was FOR
   until you had read the controls inside it and inferred it. `section()` in app.js is now the
   only way a page builds one: title, optional one-line description, optional actions on the
   right, body. `.card` stays as the bare surface — the keyboard wrapper and the modal are
   surfaces without being sections. */
.card,.sec{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-4);
  box-shadow:var(--e-1)}
.card{padding:var(--sp-5)}
.card h2{font-size:12px;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);margin:0 0 14px;font-weight:700}
.sec{overflow:hidden}
/* The head/body hairline is doing most of the work: it is what turns a padded box into a titled
   section, and it costs nothing on a black UI where a border IS the elevation. */
.sec-head{display:flex;align-items:flex-start;gap:var(--sp-4);
  padding:var(--sp-4) var(--sp-5) var(--sp-3);border-bottom:1px solid var(--line)}
.sec-title{flex:1;min-width:0}
.sec-head h2{font-size:15px;font-weight:700;letter-spacing:.1px;color:var(--ink);text-transform:none}
/* Prose, in the text face rather than the mono one. Long explanations set in 11px uppercase-ish
   mono is the other half of why this looked like a bench tool: mono is for values, not sentences. */
.sec-desc{margin:5px 0 0;font-size:12.5px;line-height:1.55;color:var(--muted);max-width:70ch}
.sec-desc b{color:var(--ink);font-weight:600}
.sec-act{display:flex;align-items:center;gap:var(--sp-2);flex:none}
.sec-body{padding:var(--sp-4) var(--sp-5) var(--sp-5)}
.sec-body>:first-child{margin-top:0}
.sec-body>:last-child{margin-bottom:0}
/* A section whose own switch is off. The controls stay readable and stay reachable — they just
   stop claiming to be in effect, which is what a rapid-trigger slider on a key with rapid
   trigger disabled was doing. */
.sec.is-off .sec-body{opacity:.42}

/* ---- Macro build steps -------------------------------------------------------------------
   The Macros page is a sequence, not four independent panels, because the ORDER MATTERS ON THE
   HARDWARE: the playback mode is written to the key at assign time, so a mode picked afterwards
   never reaches the keyboard. The numbering is the instruction. A step is dimmed until every
   step before it is done, which for a finished macro means nothing is dimmed and it reads as an
   ordinary editor again. */
.mstep .sec-head{align-items:center}
.mstep-n{flex:none;width:26px;height:26px;border-radius:var(--r-pill);display:grid;place-items:center;
  font-family:'Red Hat Mono',monospace;font-size:12px;font-weight:700;
  background:var(--panel3);color:var(--muted);border:1px solid var(--line2)}
.mstep.is-active .mstep-n{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.mstep.is-done .mstep-n{background:rgba(55,209,120,.16);color:var(--ok);border-color:var(--ok)}
/* Locked steps keep their title legible — the point is to show what is coming, not to hide it. */
.mstep.is-locked .sec-head h2,
.mstep.is-locked .sec-desc{color:var(--muted2)}
.mstep-lock{margin:0;color:var(--muted2)}
.mac-rep-h{margin:0 0 0 10px;flex:1}
/* Two sections side by side where they are genuinely peers (Lighting's effect and color). */
.sec-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));
  gap:var(--sp-4);align-items:start}
/* Page-level actions that belong to no one section — Save / Read / Delete on Macros. */
.actionbar{display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;
  background:var(--panel);border:1px solid var(--line);border-radius:var(--r-4);
  padding:var(--sp-3) var(--sp-4);box-shadow:var(--e-1)}
.actionbar .spacer{flex:1}
/* Body prose. Distinct from .hint, which stays the mono caption under a control. */
.note{font-size:12.5px;line-height:1.65;color:var(--muted);margin:var(--sp-3) 0 0;max-width:74ch}
.note:first-child{margin-top:0}
.note b{color:var(--ink);font-weight:600}
/* A .note that happens to be a list. Bare <ul> has no indent in the reset, so the markers sit
   outside the text column and the whole block reads as ragged prose. Used by the Firmware page's
   "shipped setup" summary, where the point is that it is an enumerable list of things. */
ul.note{padding-left:18px;margin:var(--sp-3) 0 0}
ul.note li{margin:3px 0}
.note.warn b,.note.ok b,.note.danger b{color:inherit}

/* persistent keyboard + scope bar */
.kbwrap{padding:14px 16px;position:relative}
.scopebar{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.scope-label{font-family:'Red Hat Mono',monospace;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted)}
#board{position:relative;margin:2px auto}
.key{position:absolute;background:var(--key);border:1px solid var(--key-line);border-radius:6px;overflow:hidden;
  display:flex;align-items:flex-end;justify-content:center;cursor:pointer;user-select:none;transition:border-color .1s,box-shadow .1s}
.key:hover{border-color:#6a6a72}
.key.selected{border-color:#fff;box-shadow:0 0 0 1px #fff}
/* Drag-select. Built to sit in the same family as the panels: the app's own corner radius, a
   panel-dark translucent fill and the same hairline border every card uses, rather than the
   default blue OS lasso. `position:fixed` because it is drawn in viewport coordinates from
   pointer events, and it sits on <body> so the board's own stacking and overflow cannot clip it. */
.band{position:fixed;z-index:60;pointer-events:none;border-radius:var(--r-1);
  border:1px solid rgba(255,255,255,.30);background:rgba(12,12,13,.45);
  box-shadow:0 0 0 1px rgba(0,0,0,.55),0 10px 30px rgba(0,0,0,.5)}
/* Caught by the band but not committed yet. Dimmer than .selected on purpose: it is a preview of
   what releasing would do, so it must not be mistaken for the selection itself. */
.key.in-band{border-color:rgba(255,255,255,.75);box-shadow:0 0 0 1px rgba(255,255,255,.35)}
/* ...and the same preview on the 3D board, which is the one anyone actually sees. The rule above
   styles `.key` itself, and `.b3d-key` sets `border:0` and is invisible by construction — every
   piece of 3D board state is drawn on the child quad. So the band DID mark keys as it swept them,
   on an element with nothing to show, and the highlight only appeared at the end when the release
   committed the set and `.selected` (which does have a quad rule) took over. It read as a band
   that only worked on release. Dimmer than `.selected` on purpose: it is a preview of what
   letting go would do, and must not be mistaken for the selection itself. */
.b3d-key.in-band .b3d-quad{border-color:rgba(255,255,255,.8);
  box-shadow:0 0 0 1px rgba(255,255,255,.4);background:rgba(255,255,255,.09)}
/* A drag across a board full of labels would otherwise paint a text selection over the caps —
   and now that a band can start anywhere in the card, over the scope bar's text too. */
#board.banding,.kbwrap.banding{user-select:none;-webkit-user-select:none}
.key.active{border-color:#fff}
.key-fill{position:absolute;left:0;bottom:0;width:100%;height:0;background:linear-gradient(0deg,var(--fill-b),var(--fill-a));opacity:.9;transition:height .07s ease-out}
.key-label{position:relative;z-index:2;font-size:10px;padding-bottom:3px;color:var(--ink);font-family:'Red Hat Mono',monospace;text-shadow:0 1px 2px #000;pointer-events:none;text-align:center;line-height:1.05;mix-blend-mode:difference}
.key-val{position:absolute;top:2px;left:0;width:100%;text-align:center;z-index:2;font-size:9px;color:#fff;font-family:'Red Hat Mono',monospace;pointer-events:none;mix-blend-mode:difference}
/* per-key configured value (actuation / rapid trigger pages), centered like Wootility */
.key-set{position:absolute;top:50%;left:0;width:100%;transform:translateY(-55%);text-align:center;z-index:2;
  font-size:11px;font-weight:700;color:#fff;font-family:'Red Hat Mono',monospace;pointer-events:none;
  mix-blend-mode:difference;line-height:1;display:none}
.key.showset .key-set{display:block}
.key.showset .key-label{font-size:8px;opacity:.55}
.key-set.off{opacity:.35;font-weight:400}
/* Media-transport caps draw a glyph instead of a word — block+auto kills the inline baseline gap
   so it sits dead-center in the cap the way the text label does. */
.key-set .icn{display:block;margin:0 auto}
/* Small tag over a cap's value, naming which number it is. Only the Rapid trigger page uses one:
   it and the Actuation page print the same-looking bold millimeter figure in the same spot, and
   an RT sensitivity of 0.50 on a key actuating at 1.00 read as a changed actuation point. */
.ks-tag{display:block;font-style:normal;font-size:7px;font-weight:700;letter-spacing:.1em;
  line-height:1;opacity:.7;margin-bottom:1px}

/* controls */
.row{display:grid;gap:7px;margin:var(--sp-4) 0}
/* The label typography is shared; the flex layout is NOT. `.row` stacks a label over a control and
   the label itself holds a number box on its right, which is what the flex line is for. `.rowline`
   puts label and control side by side and its label is just a word.
   Split apart 2026-09-02: the rule was `.row>.rlabel` only, so the one `.rowline>.rlabel` in the app
   (Actuation's "Presets") silently fell back to the body font and read as a different typeface
   sitting among a column of mono labels. */
.row>.rlabel,.rowline>.rlabel{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.row>.rlabel{display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-3)}
.row output{color:#fff;font-family:'Red Hat Mono',monospace;font-size:13px}
/* The typed value beside a slider. Sized and colored to read as the readout it replaced, so the
   rows still scan as label-then-number; it only announces itself as editable on hover/focus. The
   spinners are hidden because they step by the slider's own step (0.01 mm), which makes them a
   100-click trip across the range and an accidental scroll-wheel edit waiting to happen. */
.numbox{display:inline-flex;align-items:baseline;gap:3px}
.numbox input[type=number]{width:5.5ch;padding:1px 3px;background:none;border:1px solid transparent;
  border-radius:var(--r-1);color:#fff;font-family:'Red Hat Mono',monospace;font-size:13px;
  text-align:right;-moz-appearance:textfield;appearance:textfield}
.numbox input[type=number]::-webkit-outer-spin-button,
.numbox input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.numbox input[type=number]:hover{border-color:var(--line2);background:var(--panel2)}
.numbox input[type=number]:focus{outline:none;border-color:#5a5a63;background:var(--panel2)}
.numbox i{font-style:normal;font-family:'Red Hat Mono',monospace;font-size:13px;color:var(--muted)}
/* A caption belongs to the control above it, so it sits tight against it rather than floating
   halfway between two controls. */
.row+.hint,.row+.note{margin-top:calc(var(--sp-2) * -1 + 2px)}

/* A stock range input paints one flat rail either side of the thumb, which reads as an empty
   slot with a dot in it. `--pos` is written per input by trackPos() in app.js — delegated, so a
   slider added later fills itself without being wired up. */
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:16px;margin:0;
  background:none;cursor:pointer}
input[type=range]::-webkit-slider-runnable-track{height:5px;border-radius:var(--r-pill);
  background:linear-gradient(90deg,#fff 0 var(--pos,0%),var(--line2) var(--pos,0%));
  box-shadow:0 1px 0 rgba(255,255,255,.04) inset}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;margin-top:-5px;
  border-radius:50%;background:#fff;border:0;
  box-shadow:0 0 0 1px rgba(0,0,0,.65),0 2px 8px rgba(0,0,0,.7);transition:transform var(--dur)}
input[type=range]:hover::-webkit-slider-thumb{transform:scale(1.14)}
input[type=range]:active::-webkit-slider-thumb{transform:scale(1.04)}
input[type=range]:focus-visible{box-shadow:none}
input[type=range]:focus-visible::-webkit-slider-thumb{box-shadow:var(--ring)}
input[type=range]:disabled{opacity:.4;cursor:not-allowed}
input[type=range]::-moz-range-track{height:5px;border-radius:var(--r-pill);background:var(--line2)}
input[type=range]::-moz-range-progress{height:5px;border-radius:var(--r-pill);background:#fff}
input[type=range]::-moz-range-thumb{width:15px;height:15px;border:0;border-radius:50%;background:#fff}

select,input[type=text],input[type=number]{background:var(--panel2);color:var(--ink);
  border:1px solid var(--line2);border-radius:var(--r-1);padding:9px 10px;
  font-family:'Red Hat Text';font-size:13px;
  transition:border-color var(--dur),background var(--dur)}
select:hover,input[type=text]:hover,input[type=number]:hover{border-color:#45454d}
/* The POPUP, which is a separate surface from the control and does not inherit its background. Any
   select that sets its own transparent background (.ms-assign does, so the callout shows through)
   would otherwise drop white-on-white options into a black app. Stated for every select rather than
   just that one, so the next transparent select cannot reintroduce it. */
select option{background:var(--panel2);color:var(--ink)}
select option:checked{background:var(--panel3)}
/* The section headers in a grouped menu (Click, DPI, Scroll…). Same surface as the options, or the
   browser draws each label row on its default white background — a bright bar between every section
   in the button-assignment menu. */
select optgroup{background:var(--panel2);color:var(--muted)}
/* Own chevron, so the control matches everything else instead of dropping the OS widget into
   the middle of the page. Inline SVG, no request. */
select{appearance:none;-webkit-appearance:none;padding-right:30px;cursor:pointer;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%238b8b90' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat:no-repeat;background-position:right 10px center}

.pill{display:inline-flex;gap:2px;flex-wrap:wrap;background:var(--panel2);
  border:1px solid var(--line);border-radius:var(--r-2);padding:3px}
.pill button{background:transparent;color:var(--muted);border:0;box-shadow:none;
  font-family:'Red Hat Mono',monospace;font-size:11px;text-transform:uppercase;padding:6px 12px;
  border-radius:var(--r-1);font-weight:600}
.pill button:hover{background:var(--panel3);color:var(--ink)}
.pill button.on{background:#fff;color:#000;box-shadow:var(--e-lift)}
.pill button.on:hover{background:#fff}
.pill button:disabled{opacity:.5}
.pill button:disabled:hover{background:transparent}
/* toggle switch */
.toggle{position:relative;display:inline-block;width:42px;height:24px;flex:none;vertical-align:middle}
/* The same switch sitting in a section header, where it turns the whole section on and off. */
.hswitch{gap:10px;letter-spacing:.8px}
/* Rapid trigger's "set press and release separately" line. Rapid trigger ships as ONE slider
   driving both values (js/app.js rtControls); this is what breaks it into two. It sits under the
   slider rather than in the section header, where the header switch already means something very
   different -- that one turns rapid trigger off entirely. */
.rt-split{margin-top:var(--sp-4)}
.rt-split-l{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted);
  text-transform:uppercase;letter-spacing:.5px}
.sec.is-off .rt-split-l{color:var(--muted2)}
.sec.is-off .hswitch{color:var(--muted2)}
.toggle input{opacity:0;width:0;height:0}
.toggle span{position:absolute;inset:0;background:var(--line2);border-radius:var(--r-pill);
  transition:background var(--dur);cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,.5) inset}
.toggle span::before{content:'';position:absolute;width:18px;height:18px;left:3px;top:3px;
  background:#fff;border-radius:50%;transition:transform var(--dur);
  box-shadow:0 1px 3px rgba(0,0,0,.55)}
.toggle input:checked+span{background:var(--ok)}
.toggle input:checked+span::before{transform:translateX(18px)}
.toggle input:focus-visible+span{box-shadow:var(--ring)}

/* page head + quick-settings grid */
.page-head{display:flex;align-items:baseline;gap:var(--sp-3);flex-wrap:wrap;
  margin:2px 0 var(--sp-1)}
.page-head h1{font-size:28px;letter-spacing:-.4px;line-height:1.1}
.page-head .sub{color:var(--muted);font-size:13px}
/* Same reasoning as .light-grid and .fw-grid: no `align-items:start`, so side-by-side cards end
   level however much copy each carries. Three grids had it and all three looked lopsided. */
.quick-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--sp-4)}
/* "Full page →", in a section's actions slot. */
.more{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted);text-decoration:none;
  cursor:pointer;white-space:nowrap;border-radius:var(--r-1)}
.more:hover{color:#fff}
/* The headline number. It leads its section — the value first, then the control that changes
   it — so a glance answers "what is it set to" without reading a slider position. */
.big-val{font-family:'Red Hat Display';font-weight:900;font-size:34px;line-height:1;
  letter-spacing:-.5px;font-variant-numeric:tabular-nums}
.big-val small{font-size:15px;color:var(--muted);font-weight:500;letter-spacing:0}
.rowline{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 0}
.hint{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted2);line-height:1.5;margin:8px 0 0}
.danger{color:var(--err)}
/* ---- DKS editor (Wooting-style: 4 bindings × 4 parts of a keypress) ---- */
.dks{margin:14px 0}
.dks-head{display:grid;grid-template-columns:150px repeat(4,1fr);gap:0 6px;align-items:end}
.dks-grp{grid-column:span 2;text-align:center;font-family:'Red Hat Display';font-weight:700;
  font-size:12px;color:var(--ink);padding:6px 0;border-bottom:1px solid var(--line2);margin-bottom:8px}
.dks-col{text-align:center;font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted);line-height:1.35}
.dks-col u{display:block;color:var(--ink);text-decoration:none;font-size:11px}
.dks-col .ar{font-size:14px;color:var(--muted)}
.dks-row{display:grid;grid-template-columns:150px repeat(4,1fr);gap:6px;align-items:center;margin:6px 0}
.dks-row select{font-size:12px;padding:6px}
.dcell{position:relative;height:34px;border:1px dashed var(--line2);border-radius:6px;background:var(--panel2);
  cursor:pointer;display:grid;place-items:center;color:var(--muted2);font-size:15px;font-family:'Red Hat Mono',monospace}
.dcell:hover{border-color:#6a6a72;color:var(--ink)}
.dcell.on{background:#fff;color:#000;border:1px solid #fff;font-weight:700}
.dcell.span{background:var(--panel3);border-style:solid;border-color:var(--line2);color:var(--muted)}
.dcell.dis{opacity:.35;cursor:not-allowed}
.dks-legend{font-family:'Red Hat Mono',monospace;font-size:10.5px;color:var(--muted2);margin-top:10px;line-height:1.6}
.dks-say{font-size:12.5px;color:var(--ink);margin:10px 0 0;line-height:1.5}
.dks-presets{display:flex;gap:10px;flex-wrap:wrap;margin:4px 0 14px}
.dpreset{flex:1;min-width:210px;text-align:left;background:var(--panel2);border:1px solid var(--line2);
  border-radius:var(--r-3);padding:11px 13px;cursor:pointer;color:var(--ink);box-shadow:none}
.dpreset:hover{border-color:#6a6a72;background:var(--panel3)}
.dpreset b{display:block;font-family:'Red Hat Display';font-size:13px;margin-bottom:3px}
.dpreset span{font-family:'Red Hat Text';font-size:11.5px;color:var(--muted)}
.kchip{display:inline-block;background:var(--panel3);border:1px solid var(--line2);border-radius:5px;
  padding:2px 7px;font-family:'Red Hat Mono',monospace;font-size:10px;margin-right:4px;color:var(--ink)}

/* Raw op-8 byte readout under the DKS grid — bench only (?dev=1), hidden otherwise. */
.bytes{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted2)}

/* ---- Firmware page ---- */
.fw-hero{display:flex;align-items:flex-start;gap:34px;flex-wrap:wrap}
.fw-hero .rlabel{display:block;font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted);
  text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.fw-hero .big-val{font-size:40px}
.fw-meta{flex:1;min-width:260px}
.fw-meta .rowline{margin:0;padding:8px 0;border-top:1px solid var(--line);font-size:12.5px}
.fw-meta .rowline:first-child{border-top:0;padding-top:0}
.fw-meta .rowline span{color:var(--muted)}

/* ---- splash / connect gate ---- */
#splash{position:fixed;inset:0;z-index:100;display:grid;place-items:center;
  background:radial-gradient(circle at 50% 38%,#141417 0%,#000 62%);
  opacity:1;transition:opacity .45s ease}
#splash.gone{opacity:0;pointer-events:none}
.sp-inner{display:flex;flex-direction:column;align-items:center;gap:0;padding:32px;text-align:center;
  animation:spin-up .5s ease both}
@keyframes spin-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.sp-mark{width:76px;height:76px;border-radius:50%;background:#fff;display:grid;place-items:center;
  box-shadow:0 12px 44px rgba(0,0,0,.7)}
.sp-mark img{width:49px;height:49px;display:block}
.sp-word{font-family:'Range Unique','Red Hat Display',sans-serif;font-size:44px;letter-spacing:2px;
  line-height:1;color:#fff;margin-top:22px}
.sp-tag{font-family:'Red Hat Mono',monospace;font-size:11px;letter-spacing:3px;text-transform:uppercase;
  color:var(--muted);margin-top:12px}
.sp-block{display:none;flex-direction:column;align-items:center;gap:14px;margin-top:34px;width:340px;max-width:86vw}
#splash[data-phase="connect"] .sp-block[data-for="connect"],
#splash[data-phase="reading"] .sp-block[data-for="reading"],
#splash[data-phase="error"]   .sp-block[data-for="error"],
#splash[data-phase="unsupported"] .sp-block[data-for="unsupported"]{display:flex}
#splash[data-phase="stuck"] .sp-block[data-for="stuck"]{display:flex}
/* The unsupported-browser block carries two paragraphs rather than a button, so it needs the
   width and the sentence case that the one-line ledes don't. */
#splash .sp-block[data-for="unsupported"]{width:430px}
#splash .sp-block[data-for="unsupported"] .sp-note{text-transform:none;letter-spacing:0;
  font-size:11.5px;line-height:1.65}
.sp-lede{font-size:13.5px;color:var(--muted);margin:0;line-height:1.6}
.sp-lede.sp-err{color:var(--err)}
.sp-block button{padding:11px 26px;font-size:14px}
.sp-block button.sp-skip{padding:6px 12px;font-size:11px;color:var(--muted2);border-color:var(--line)}
.sp-block button.sp-skip:hover{color:var(--ink)}
/* Connect and "look around" on one line. Stacked, two buttons read as a decision to make; side by
   side with the secondary one small and outlined, it reads as one action and an escape hatch. */
.sp-actions{display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap}
/* The recovery link, parked in the splash's bottom-right. Offered only in the phases where it
   could help: while connecting, or after a failure. During the read the board is plainly fine, and
   the stuck-recovery block must not offer a link back to itself. */
.sp-stuck-corner{position:absolute;right:18px;bottom:16px;display:none;font-size:11px}
#splash[data-phase="connect"] .sp-stuck-corner,
#splash[data-phase="error"] .sp-stuck-corner{display:block}
.sp-bar{width:100%;height:6px;border-radius:4px;background:var(--panel3);overflow:hidden}
.sp-fill{display:block;height:100%;width:0;border-radius:4px;background:#fff;transition:width .3s ease}
.sp-meta{display:flex;justify-content:space-between;width:100%;font-family:'Red Hat Mono',monospace;
  font-size:11px;color:var(--muted)}
.sp-meta #sp-pct{color:#fff}
.sp-note{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted2);margin:0;
  text-transform:uppercase;letter-spacing:1px}
/* Recovery block: prose and a numbered sequence, so it opts out of the uppercase mono the one-line
   notes use — same reasoning as the unsupported block above. */
#splash .sp-block[data-for="stuck"]{width:430px}
#splash .sp-block[data-for="stuck"] .sp-note{text-transform:none;letter-spacing:0;
  font-size:11.5px;line-height:1.65}
.sp-steps{margin:0;padding-left:20px;text-align:left;align-self:stretch;font-size:12.5px;
  color:var(--muted);line-height:1.7}
.sp-steps b{color:var(--ink)}
/* A text-weight affordance: present for the person who needs it, ignorable by everyone else. */
button.linky{background:none;border:none;padding:2px 4px;font-size:11px;color:var(--muted2);
  text-decoration:underline;cursor:pointer}
button.linky:hover{color:var(--ink)}
/* Same recovery on the Firmware page, collapsed — a healthy board should not be told it might be
   broken, but the entry point has to exist because a stuck board looks identical from in there. */
.fw-stuck{margin:0 0 14px;padding:10px 14px;border:1px solid var(--line);border-radius:var(--r-3)}
.fw-stuck summary{cursor:pointer;font-size:12.5px;color:var(--muted2)}
.fw-stuck summary:hover{color:var(--ink)}
.fw-stuck ol{padding-left:20px;line-height:1.7}
.fw-stuck button{margin-top:6px}
@media (prefers-reduced-motion:reduce){.sp-inner{animation:none}#splash{transition:none}}

/* ---- board pick mode (assign a macro, bind Snap Tap) ---- */
.pickbar{display:flex;align-items:center;gap:10px;margin:0 0 10px;padding:10px 14px;border-radius:var(--r-3);
  background:rgba(55,209,120,.1);border:1px solid var(--ok);font-size:13px}
.pickbar b{font-weight:600}
.pickbar .spacer{flex:1}
.pickbar .pb-dot{width:9px;height:9px;border-radius:50%;background:var(--ok);flex:none;
  box-shadow:0 0 9px var(--ok);animation:recpulse 1.3s ease-in-out infinite}
.pickbar .pb-esc{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted);
  text-transform:uppercase;letter-spacing:1px}
.pickbar button{padding:5px 11px;font-size:11px}
#board.picking .key{border-color:var(--ok);box-shadow:inset 0 0 0 1px rgba(55,209,120,.25)}
#board.picking .key:hover{background:rgba(55,209,120,.22);border-color:#fff;transform:translateY(-1px)}
#board.picking .key.pick-done{border-color:#fff;box-shadow:0 0 0 2px #fff}
#board.picking .key.selected:not(.pick-done){border-color:var(--ok);box-shadow:none}
@media (prefers-reduced-motion:reduce){.pickbar .pb-dot{animation:none}}

/* keys carrying a macro, shown on the Macros page */
.key.macro-bound{border-color:var(--line2)}
.key.macro-bound .key-set{color:var(--muted)}
.key.macro-this{border-color:#fff;box-shadow:0 0 0 1px #fff,0 0 14px rgba(255,255,255,.25)}

/* keys carrying an advanced mode (Snap Tap / Multi-Tap / DKS / Toggle), on the Advanced page */
.key.adv-set{border-color:var(--line2)}
.key.adv-set .key-set{font-size:9px;letter-spacing:.3px}

/* ---- calibration ---- */
.key.cal-done{border-color:var(--ok);box-shadow:inset 0 0 0 1px rgba(55,209,120,.45)}
.key.cal-done .key-label{color:var(--ok)}
.cal-live{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--err);
  text-transform:uppercase;letter-spacing:1px}
#cal-out .mac-steps{max-height:300px}
#cal-out .mac-key{flex:0 0 70px}

/* ---- Macros ---- */
.mac-note{display:flex;align-items:center;gap:10px;padding:11px 15px;border-radius:var(--r-3);font-size:13px;
  border:1px solid var(--line2);background:var(--panel2)}
.mac-note.ok{border-color:var(--ok);background:rgba(55,209,120,.1)}
.mac-note.err{border-color:var(--err);background:rgba(255,84,112,.1)}
.mac-note span{flex:1}
.mac-note button{padding:3px 9px;font-size:11px}
.mac-keys{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.mac-chip{display:inline-flex;align-items:center;gap:7px;padding:6px 8px 6px 12px;border-radius:8px;
  background:var(--panel3);border:1px solid var(--line2);font-family:'Red Hat Mono',monospace;font-size:12px}
.mac-chip-x{background:transparent;color:var(--muted);border:0;padding:0 3px;font-size:12px;line-height:1;box-shadow:none}
/* Restates its own background. A button that styles itself but leaves :hover to inherit gets the
   default white-button hover instead, because `button:hover` (0,1,1) out-specifies the plain
   class that set the resting background (0,1,0) — so the ✕ flashed white on hover. */
.mac-chip-x:hover{color:var(--err);background:transparent;filter:none}
/* The playback mode the BOARD has for this key. `stale` = the editor's selection hasn't been
   saved yet, so what you see in the Playback pill is not what the keyboard is doing. */
.mac-chip-mode{padding:1px 6px;border-radius:5px;background:var(--panel2);color:var(--muted);
  font-size:10px;letter-spacing:.04em;text-transform:uppercase}
.mac-chip.stale{border-color:var(--warn)}
.mac-chip.stale .mac-chip-mode{background:rgba(242,169,0,.16);color:var(--warn)}
.mac-add{font-size:12px;padding:7px 13px}
.mac-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:4px 0 10px}
.mac-bar .spacer{flex:1}
#mac-rec.recording{background:var(--err);color:#fff;animation:recpulse 1.1s ease-in-out infinite}
@keyframes recpulse{0%,100%{box-shadow:0 0 0 0 rgba(255,84,112,.55)}50%{box-shadow:0 0 0 7px rgba(255,84,112,0)}}
@media (prefers-reduced-motion:reduce){#mac-rec.recording{animation:none}}
.mac-meter{width:120px;height:5px;border-radius:3px;background:var(--panel3);overflow:hidden;flex:none}
.mac-meter-fill{display:block;height:100%;background:#fff;transition:width .12s}
.mac-count{font-family:'Red Hat Mono',monospace;font-size:11px;color:var(--muted)}
.hint.mac-live{color:var(--err)}
.mac-empty{margin:0}
.mac-steps{display:flex;flex-direction:column;gap:4px;max-height:340px;overflow:auto;margin-top:4px}
.mac-step{display:flex;align-items:center;gap:12px;
  background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px}
.mac-n{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted2);width:18px;text-align:right;flex:none}
.mac-key{font-family:'Red Hat Mono',monospace;font-size:12px;color:var(--ink);font-weight:600;
  flex:0 0 86px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mac-act{font-family:'Red Hat Mono',monospace;font-size:10px;text-transform:uppercase;
  letter-spacing:.5px;flex:0 0 92px}
.mac-act.down{color:var(--ok)} .mac-act.up{color:var(--muted)} .mac-act.tap{color:var(--muted)}
.mac-times{display:flex;gap:14px;flex-wrap:wrap;flex:1}
.mac-t{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted2);white-space:nowrap;
  display:inline-flex;align-items:center;gap:3px}
.mac-t input{width:66px;padding:4px 6px;font-size:11px;text-align:right;
  font-family:'Red Hat Mono',monospace}
.mac-move{display:flex;gap:3px;margin-left:auto;flex:none}
#mac-view button{font-size:10px;padding:5px 9px}
.mac-move button{padding:3px 8px;font-size:11px;line-height:1}
.mac-rep input{width:110px}
button.ghost.danger{color:var(--err);border-color:var(--err)}
button.ghost.danger:hover{background:rgba(255,84,112,.12)}

/* ---- Lighting: Effect and Color side by side ----
   Not the plain .sec-grid, because these two columns are not peers in width. The color wheel is
   a fixed 196px stage plus its readout beside it, and equal columns land ~6px short of fitting
   both — so the wheel wraps under its own numbers and reads as a layout accident rather than a
   choice. Weighting the color column carries the wheel with room to spare. */
/* align-items defaults to `stretch`, which is what makes the two cards the same height however
   much copy each one carries. It used to be `start`, so they ended wherever their content did and
   the row looked lopsided. Anything inside that must NOT stretch says so itself. */
.light-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.18fr);
  gap:var(--sp-4)}
@media (max-width:1080px){.light-grid{grid-template-columns:1fr}}

/* ---- magnetic switch cutaway (js/switchview.js) ----
   Switch on the left, travel axis on the right, sliders beside it. Below the breakpoint the
   drawing goes full width rather than shrinking into a stripe — it is the explanation, and a
   250px-wide explanation next to a 250px-wide control column explains nothing. */
.sw-cols{display:grid;grid-template-columns:minmax(0,392px) minmax(260px,1fr);gap:26px;align-items:start}
@media (max-width:900px){.sw-cols{grid-template-columns:1fr}}
.sw-ctl .row:first-child{margin-top:0}
/* The render and the scale, side by side — the composition both reference apps use. The render
   box is sized up front so the scale does not jump when the 2.8 MB mesh lands. */
.swv{display:flex;align-items:stretch;gap:12px;width:100%;max-width:392px;margin:0 auto}
.swv-3d{flex:1 1 auto;min-width:120px;height:250px;border-radius:12px;background:
  radial-gradient(120% 90% at 50% 18%,#1a1a1e 0%,#0e0e10 70%);border:1px solid var(--line)}
.swv-3d canvas{display:block;width:100%;height:100%}
.swv-scale{flex:0 0 196px;height:250px;overflow:visible}

.swv-tick-label{fill:var(--muted2);font-family:'Red Hat Mono',monospace;font-size:10px}
.swv-axis-unit{fill:var(--muted2);font-family:'Red Hat Mono',monospace;font-size:9px;
  text-transform:uppercase;letter-spacing:1px}
.swv-axis-line,.swv-tick{stroke:var(--line2);stroke-width:1}

/* The live readout, riding the live position. Both references label their travel bar this way,
   and it is the one number that changes continuously — so it goes to the reading rather than
   making the reader look somewhere else for it. */
.swv-chip-box{fill:var(--panel3);stroke:var(--line2);stroke-width:1}
.swv-chip-text{fill:var(--ink);font-family:'Red Hat Mono',monospace;font-size:11px}
.swv.is-on .swv-chip-box{fill:rgba(55,209,120,.16);stroke:var(--ok)}
.swv.is-on .swv-chip-text{fill:var(--ok)}
.swv-band{fill:var(--muted2);opacity:.5}
.swv-mark{stroke-width:1.5}
.swv-mark-act{stroke:var(--ink)}
/* Lift-off: distinct hue AND dashed, so where it coincides with the solid actuation line (the
   default, since lift-off defaults to the actuation depth) both are still legible. */
.swv-mark-lift{stroke:var(--lift);stroke-width:1.5;stroke-dasharray:4 3}
.swv-mark-label{font-family:'Red Hat Mono',monospace;font-size:10.5px;letter-spacing:.2px}
.swv-mark-label.swv-mark-act{fill:var(--ink);stroke:none}
.swv-mark-label.swv-mark-lift{fill:var(--lift);stroke:none}
/* Leaders join a de-collided label back to its own line when the two points sit close together. */
.swv-lead{stroke-width:1;opacity:.5}
.swv-lead-act{stroke:var(--ink)}
.swv-lead-lift{stroke:var(--lift)}
/* Rapid-trigger threshold: a dashed amber line across the track at the depth where the next event
   fires. Dashed so it reads as the moving RT threshold, not the solid ink actuation line. */
.swv-rt-bracket{fill:none;stroke:var(--warn);stroke-width:1.5;stroke-dasharray:5 3}
.swv-rt-label{fill:var(--warn);font-family:'Red Hat Mono',monospace;font-size:9px}

.swv-live-line{stroke:var(--ink);stroke-width:2}
.swv-live-fill{fill:var(--ink);opacity:.30}
/* Actuated. Green is the app's "registered / good" color everywhere else, and this is the one
   moment the whole page is about, so the scale says it rather than leaving it to be read off a
   number. No transition: the depth stream is already eased in JS, and a CSS fade on top would
   lag the cap it is supposed to be describing. */
.swv.is-on .swv-live-line{stroke:var(--ok)}
.swv.is-on .swv-live-fill{fill:var(--ok);opacity:.42}
/* Rapid trigger: the fill lights AMBER only while the RT state machine (updateRt in switchview.js)
   says the key is fired, and falls back to the dim idle fill the instant it releases — so you can
   watch it actuate at the press threshold and drop at the release threshold as the key moves. Amber
   (not the green fixed-actuation color) because RT is a different behavior, and it matches the RT
   bracket. The two-class selector outranks the one-class .is-on green, so fired-under-RT stays
   amber regardless of source order. */
.swv.is-rt.is-on .swv-live-fill{fill:var(--warn);opacity:.5}
.swv.is-rt.is-on .swv-live-line{stroke:var(--warn)}
.swv.is-rt.is-on .swv-chip-box{fill:rgba(242,169,0,.16);stroke:var(--warn)}
.swv.is-rt.is-on .swv-chip-text{fill:var(--warn)}

/* ---- color wheel ---- */
.cw{position:relative;display:flex;gap:18px;flex-wrap:wrap}
.cw-stage{display:flex;gap:12px;flex:none}
.cw-wheel{position:relative;border-radius:50%;flex:none;cursor:crosshair;outline:none;
  box-shadow:0 0 0 1px var(--line2),0 8px 26px rgba(0,0,0,.55);touch-action:none}
.cw-wheel:focus-visible{box-shadow:0 0 0 2px #fff,0 8px 26px rgba(0,0,0,.55)}
.cw-wheel canvas{display:block;border-radius:50%}
.cw-knob{position:absolute;width:18px;height:18px;margin:-9px 0 0 -9px;border-radius:50%;
  border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.65),0 2px 8px rgba(0,0,0,.6);pointer-events:none}
.cw-bar{position:relative;width:22px;border-radius:11px;flex:none;cursor:ns-resize;outline:none;
  box-shadow:0 0 0 1px var(--line2);touch-action:none}
.cw-bar:focus-visible{box-shadow:0 0 0 2px #fff}
.cw-bar-knob{position:absolute;left:50%;width:24px;height:24px;margin:-12px 0 0 -12px;border-radius:50%;
  border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.65),0 2px 8px rgba(0,0,0,.6);pointer-events:none}
.cw-side{flex:1;min-width:190px;max-width:270px;display:flex;flex-direction:column;gap:10px}
.cw-preview{display:flex;align-items:center;gap:10px}
.cw-chip{width:38px;height:38px;border-radius:9px;flex:none;box-shadow:0 0 0 1px var(--line2),0 0 18px rgba(255,255,255,.06)}
.cw-meta b{font-family:'Red Hat Mono',monospace;font-size:15px;letter-spacing:.5px;display:block}
.cw-meta small{font-family:'Red Hat Mono',monospace;font-size:10px;color:var(--muted2)}
.cw-fields{display:grid;grid-template-columns:1.6fr repeat(3,1fr);gap:8px}
.cw-fields label{display:flex;flex-direction:column;gap:4px;font-family:'Red Hat Mono',monospace;
  font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted2)}
.cw-fields input{width:100%;padding:6px 7px;font-family:'Red Hat Mono',monospace;font-size:12px;text-align:center}
.cw-cap{font-family:'Red Hat Mono',monospace;font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted2)}
.cw-swatches{display:flex;gap:6px;flex-wrap:wrap}
.cw-sw{width:24px;height:24px;padding:0;border-radius:var(--r-1);border:1px solid rgba(255,255,255,.18);cursor:pointer;box-shadow:none}
.cw-sw:hover{filter:none;transform:scale(1.12)}
.cw-drop{align-self:flex-start;font-size:11px;padding:6px 10px}
.cw-veil{position:absolute;inset:-6px;display:none;place-items:center;border-radius:12px;
  background:rgba(0,0,0,.55);backdrop-filter:blur(1.5px);z-index:3}
.cw-veil span{max-width:280px;text-align:center;font-family:'Red Hat Mono',monospace;font-size:11px;
  line-height:1.6;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.cw.off{opacity:.55}
.cw.off .cw-veil{display:grid}
.cw.off .cw-wheel,.cw.off .cw-bar{cursor:default}
/* Both knobs are absolutely positioned with a negative margin (half their own size) so they center
   ON the value rather than beside it. That means the brightness knob overhangs the top of .cw by
   12px at full brightness — 6px further than the veil's inset:-6px reaches — and a white-bordered
   circle floated above the blur. Hiding them is the fix rather than growing the veil: the veil
   would have to be re-tuned every time the knob size or the bar height changes, and there is
   nothing to point at on a wheel you can't use. */
.cw.off .cw-knob,.cw.off .cw-bar-knob{display:none}

/* ---- 3D board (board3d.js) ----
   The WebGL canvas draws the chassis and the keycaps from the vendor CAD; the caps
   themselves travel down by the live actuation depth. The `.key` elements are still here,
   but in 3D they stop being the keyboard and become a HUD: each one is positioned every
   frame over its cap's projected screen position, and carries only the values the
   configurator writes per page. The cap's own baked legend replaces `.key-label`. */
/* Capped in width so the board does not stretch across a wide monitor — board3d derives its
   canvas height from this width, so the one cap bounds both axes.
   The canvas is a click target: a raycast on a cap selects its key, hence pointer. The view is
   locked, so it is not a drag surface — if board3d's VIEW_ADJUSTABLE is switched back on, make
   this ns-resize, since up/down is then the only axis that does anything.
   No backdrop of its own: the renderer is created with `alpha: true`, so with no CSS background
   here the canvas is genuinely transparent and the board sits on the card it lives in rather than
   in a letterboxed photo panel. Nothing to round off either, hence no border-radius — a radius
   would only clip the WebGL content at corners the board never reaches.
   (There used to be a radial gradient here. If one ever comes back, keep it NEUTRAL: the original
   #17171c/#0a0a0c pair was blue-leaning and made the jet-black board read navy against it.) */
#board.board-3d{width:100% !important;max-width:960px;height:auto;margin:0 auto}
.b3d-canvas{display:block;width:100%;height:100%;cursor:pointer;background:none}
/* Tuning readout — only present while board3d's VIEW_ADJUSTABLE is on. */
.b3d-view{position:absolute;right:10px;bottom:8px;z-index:4;pointer-events:none;
  font-family:'Red Hat Mono',monospace;font-size:10px;letter-spacing:.5px;color:var(--muted);
  background:rgba(0,0,0,.5);border:1px solid var(--line);border-radius:6px;padding:3px 7px}
/* overflow:hidden keeps a label from spilling out of the board's area and into the card; there is
   no radius to match now that the canvas has no box of its own. */
.b3d-overlay{position:absolute;inset:0;pointer-events:none;overflow:hidden}

/* The screen-aligned half: an invisible frame over the projected cap top that exists to hold
   upright text and nothing else. Left and top are pinned to 0 because board3d drives position
   through `transform` alone, and width/height are written inline every frame from the projected
   corners' bounding rect.
   It MUST NOT PAINT, and that is harder than it looks, because this element still carries the
   `key` class and a dozen flat-board rules out-specify `.b3d-key` (0,1,0):
     .key.selected        (0,2,0)  box-shadow:0 0 0 1px #fff   <- a white ring on this rectangle
     .key.macro-this      (0,2,0)  box-shadow + glow
     .key.cal-done        (0,2,0)  inset ring
     #board.picking .key  (1,2,0)  inset ring
     #board.picking .key:hover (1,3,0)  green background
     #board.rgbpreview .key    (1,2,0)  solid background + glow
   Every one of those drew the old axis-aligned box BEHIND the perspective quad — two outlines per
   key. `border:0` alone does not stop them: a box-shadow is not a border and paints regardless of
   border width. So background and box-shadow are forced off here. !important is the right tool
   rather than the lazy one — the alternative is restating each of those selectors at higher
   specificity, which silently breaks again the next time a flat-board state rule is added. */
.b3d-key{left:0;top:0;padding:0;border:0;overflow:visible;
  background:none!important;box-shadow:none!important;
  flex-direction:column;align-items:center;justify-content:center;gap:1px;
  pointer-events:none;transition:none}
/* The box itself, warped onto the cap's top face by a matrix3d homography written per frame (see
   quadMatrix in board3d.js). Every ring, border and wash belongs here, not on the parent, and this
   is the only part that takes clicks — they bubble up to the parent's listener. */
.b3d-quad{position:absolute;inset:0;transform-origin:0 0;border:1px solid transparent;
  border-radius:4px;pointer-events:auto;backface-visibility:visible}
.b3d-key .key-fill{display:none}
.b3d-key .key-label{display:none}          /* the cap carries its own legend */

/* ---- key-name overlay + its toggle ----
   The cap's own legend is FRONT-printed and translucent, exactly as on the real part, which
   reads small and dim at a 62° view — fine once you know the board, not fine when you don't.
   `#board.labels` prints the name on the cap TOP instead, opt-in per user and remembered.
   It sits below `.key-set` in the parent's flex column, so on pages that write a value the
   value stays the headline and the name is the caption beneath it; on pages that write
   nothing (`.key-set` is display:none without `.showset`) the name is alone and centerd.
   Specificity: (1,4,0), which is what it takes to beat the `.b3d-key .key-label` rule above
   and the `.key.showset .key-label` size/opacity rule from the flat board. */
#board.board-3d.labels .b3d-key .key-label{display:block;position:static;padding:0;
  transform:translate(var(--capdx,0px),var(--capdy,0px));
  font-size:9px;line-height:1.05;opacity:.75;white-space:nowrap;
  mix-blend-mode:normal;text-shadow:0 0 4px #000,0 1px 3px #000}
/* Bottom-left corner of the keyboard card. The board never reaches it — the canvas is far
   wider than the board is deep, so the strips above and below the board are empty — and the
   bottom-RIGHT is already spoken for by board3d's VIEW_ADJUSTABLE readout (.b3d-view). */
.lbl-toggle{position:absolute;left:16px;bottom:12px;z-index:4;box-shadow:none;
  display:flex;align-items:center;gap:7px;
  background:rgba(0,0,0,.55);border:1px solid var(--line2);border-radius:var(--r-pill);padding:5px 12px;
  font-family:'Red Hat Mono',monospace;font-size:10px;font-weight:400;text-transform:uppercase;
  letter-spacing:.8px;color:var(--muted)}
.lbl-toggle::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--line2);
  border:1px solid var(--muted2);transition:background .12s}
/* Same trap as .mac-chip-x:hover — this must restate its background or the default white-button
   hover wins and the corner control turns into a white pill over the render. */
.lbl-toggle:hover{filter:none;color:var(--ink);border-color:#6a6a72;background:rgba(0,0,0,.72)}
.lbl-toggle.on{color:var(--ink);border-color:#6a6a72}
.lbl-toggle.on::before{background:var(--ok);border-color:var(--ok);box-shadow:0 0 7px var(--ok)}
/* Floated out of flow on purpose, above the cap: in flow, the live depth readout appearing and
   disappearing would change the box's content height and shift the other text around, so a key
   would visibly jiggle as it was pressed — canceling out the travel the cap is trying to show.
   `top:auto` is load-bearing: the base `.key-val` rule sets `top:2px`, and with both top and
   bottom set the box was over-constrained, so its used height collapsed to 0 and `bottom` was
   ignored outright. The text still painted (the parent is overflow:visible) but from 2px BELOW the
   cap's top edge instead of just above it, which is not where either rule meant to put it. */
.b3d-key .key-val{position:absolute;top:auto;bottom:calc(100% + 1px);left:50%;transform:translateX(-50%);
  width:auto;white-space:nowrap;font-size:10px;font-weight:700;
  text-shadow:0 0 4px #000,0 1px 3px #000;mix-blend-mode:normal}
/* --capdx/--capdy are written per frame by board3d's positionOverlay: the offset from this box's
   center to the CAP QUAD's center. Without it the legend sits at the center of the corners'
   bounding rect, which is not the middle of a trapezoid, and glyphs drift off the cap the further
   they are from the view axis.
   max-width keeps a long legend ("RGB mode" on Enter) inside its own cap instead of running across
   the neighboring one; it wraps rather than clipping, because a truncated key name is useless. */
.b3d-key .key-set{position:static;transform:translate(var(--capdx,0px),var(--capdy,0px));
  width:auto;max-width:100%;text-shadow:0 0 4px #000,0 1px 3px #000;
  mix-blend-mode:normal;font-size:10px;text-align:center;line-height:1.1}
.b3d-key.showset .key-set{display:block}

/* State is a ring on the cap plus, at most, a LIGHT wash. Two things to keep in mind here:
   every one of these targets the QUAD, because the parent is invisible by construction; and the
   dark fills these rules used to carry (rgba(0,0,0,.35–.45), fine under a 22px badge) cover a
   whole jet-black keycap once the box is the size of the key — the selected key read as a hole in
   the board and the orange accents went muddy. Light washes let the cap's color through. */
.b3d-key.selected .b3d-quad{border-color:#fff;box-shadow:0 0 0 1px #fff,0 0 12px rgba(255,255,255,.45);
  background:rgba(255,255,255,.12)}
.b3d-key.active .b3d-quad{border-color:transparent;box-shadow:none}
.b3d-key.adv-set .b3d-quad,.b3d-key.macro-bound .b3d-quad{border-color:var(--line2);
  background:rgba(255,255,255,.07)}
.b3d-key.macro-this .b3d-quad{border-color:#fff;box-shadow:0 0 0 1px #fff,0 0 14px rgba(255,255,255,.25);
  background:rgba(255,255,255,.14)}
.b3d-key.cal-done .b3d-quad{border-color:var(--ok);box-shadow:0 0 0 1px rgba(55,209,120,.55);
  background:rgba(55,209,120,.16)}
.b3d-key.cal-done .key-label{color:var(--ok)}

/* Pick mode. The flat board's `#board.picking .key:hover` rule also sets a `transform`, but
   board3d writes the projection as an INLINE transform, and inline style beats any
   stylesheet rule — so the hover nudge is simply ignored here and a ring is used instead.
   :hover on the parent works even though only the quad takes pointer events, because hover
   propagates to ancestors. */
#board.board-3d.picking .b3d-quad{border-color:var(--ok);box-shadow:0 0 0 1px rgba(55,209,120,.5);
  background:rgba(55,209,120,.12)}
#board.board-3d.picking .b3d-key:hover .b3d-quad{background:rgba(55,209,120,.34);
  border-color:#fff;box-shadow:0 0 0 1px #fff}
/* A key already chosen in a multi-step pick (Snap Tap's first key while the second is chosen).
   Pick mode paints every cap green, so a chosen key had nothing to distinguish it. White ring,
   deliberately the strongest thing on the board while the picker is up. */
#board.board-3d.picking .b3d-key.pick-done .b3d-quad{border-color:#fff;
  box-shadow:0 0 0 2px #fff,0 0 16px rgba(255,255,255,.55);background:rgba(255,255,255,.20)}
/* And the ordinary selection is SUPPRESSED while picking. It is drawn in white too, so leaving it
   up meant the key you were being asked to replace looked exactly like the key you had just
   chosen. Specificity (1,4,0) beats `.b3d-key.selected .b3d-quad` (0,3,0). */
#board.board-3d.picking .b3d-key.selected:not(.pick-done) .b3d-quad{border-color:var(--ok);
  box-shadow:0 0 0 1px rgba(55,209,120,.5);background:rgba(55,209,120,.12)}
#board.board-3d.rgbpreview .b3d-quad{background:none;box-shadow:none;border-color:transparent}

/* Live effect preview on the persistent keyboard while on the Lighting page. `--rgbp` is written
   per key per frame by lightfx.js, which is also why there is no animation here any more: this
   used to be a whole-board `hue-rotate` filter for the rainbow case, and a filter on the parent
   both fought the per-key colors and forced a full repaint of the WebGL canvas underneath.
   No transition either — a 61-key board easing toward 61 new colors 60 times a second is 61
   interpolations that never finish, and the effect comes out as mush. */
#board.rgbpreview .key{background:var(--rgbp,#161619);border-color:rgba(255,255,255,.22);
  box-shadow:0 0 14px -2px var(--rgbp,transparent);transition:none}
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;place-items:center;z-index:50}
.modal-bg.on{display:grid}
.modal{background:var(--panel);border:1px solid var(--line2);border-radius:var(--r-4);
  padding:var(--sp-5);max-width:460px;width:92%;max-height:80vh;overflow:auto;box-shadow:var(--e-2)}
.modal h3{font-size:18px;margin-bottom:10px}

/* traffic dock */
.log-dock{position:fixed;bottom:0;left:0;right:0;background:#050506;border-top:1px solid var(--line);z-index:40;font-family:'Red Hat Mono',monospace}
.log-head{padding:6px 16px;font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);cursor:pointer;border-bottom:1px solid var(--line)}
#log{height:120px;overflow:auto;padding:6px 16px;font-size:11px;display:none}
.log-dock.open #log{display:block}
#log>div{white-space:pre}
.log-tx{color:#fff}.log-rx{color:var(--ok)}.log-dry{color:var(--muted2)}.log-blk{color:var(--err)}

/* ---- save-state indicator (header) --------------------------------------------------------
   Says out loud that this board has no save step — every write commits to flash as it is made.
   Doubles as progress for an "All keys" batch, which is a chained write that only commits at
   the end, so there IS a window where the board and the screen disagree. */
.saved{font-family:'Red Hat Mono',monospace;font-size:11px;display:flex;align-items:center;gap:6px;
  text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
.saved::before{content:'';width:8px;height:8px;border-radius:50%;background:var(--muted2);flex:none}
.saved[data-state="saved"]{color:var(--ok)}
.saved[data-state="saved"]::before{background:var(--ok);box-shadow:0 0 8px var(--ok)}
.saved[data-state="saving"]{color:var(--warn)}
.saved[data-state="saving"]::before{background:var(--warn);animation:sv-pulse 1s ease-in-out infinite}
.saved[data-state="error"]{color:var(--err)}
.saved[data-state="error"]::before{background:var(--err)}
@keyframes sv-pulse{0%,100%{opacity:1}50%{opacity:.25}}

/* ---- verify / copy report ----------------------------------------------------------------- */
.vrow{display:flex;align-items:baseline;gap:10px;padding:7px 0;border-top:1px solid var(--line);
  font-family:'Red Hat Mono',monospace;font-size:11px}
.vrow:first-child{border-top:0}
.vrow .vk{flex:none;width:74px;color:var(--ink);font-weight:700}
.vrow .vw{flex:1;color:var(--muted)}
.vrow .vv{flex:none;color:var(--warn)}
.vsum{font-family:'Red Hat Mono',monospace;font-size:11px;line-height:1.6;margin:0 0 12px;color:var(--muted2)}
.vsum b{color:var(--ink)}
.vbar{height:4px;border-radius:2px;background:var(--panel3);overflow:hidden;margin:10px 0}
.vbar>span{display:block;height:100%;background:var(--fill-a);width:0;transition:width .12s linear}
.vscroll{max-height:38vh;overflow:auto;margin-top:6px}

/* The Calibration and Lighting pages have always emitted .banner / .banner.info with no rule
   behind it, so those callouts rendered as bare body text. */
/* A page-level callout. Set in the text face, not mono: these are two-sentence explanations and
   an 11px mono paragraph is a wall of ticker tape. */
.banner{border:1px solid var(--line);border-left:3px solid var(--warn);border-radius:var(--r-3);
  background:var(--panel2);padding:13px 16px;font-size:12.5px;
  line-height:1.6;color:var(--muted);box-shadow:var(--e-1)}
.banner b{color:var(--ink);font-weight:600}
.banner.info{border-left-color:var(--fill-b)}

/* ---- device picker + mouse pages -------------------------------------------------------------
   The picker scopes the nav, so it sits above it inside the sidebar rather than in the header.
   Everything below borrows the existing card/row/pill primitives; only the shapes that don't
   exist yet are declared here. */
.devpick{padding:2px 4px var(--sp-3)}
.dp-label{display:block;font-family:'Red Hat Mono',monospace;font-size:10px;text-transform:uppercase;
  letter-spacing:1.5px;color:var(--muted2);margin:0 6px 6px}
.dp-wrap{position:relative}
/* The chevron is drawn rather than left to the UA, whose default arrow is the one control on the
   page that would still look like stock Windows. */
.dp-wrap::after{content:'';position:absolute;right:11px;top:50%;width:7px;height:7px;
  border-right:1.5px solid var(--muted);border-bottom:1.5px solid var(--muted);
  transform:translateY(-70%) rotate(45deg);pointer-events:none}
.dp-select{-webkit-appearance:none;appearance:none;width:100%;
  background:var(--panel2);color:var(--ink);border:1px solid var(--line2);border-radius:var(--r-2);
  padding:9px 28px 9px 11px;font:inherit;font-size:13.5px;font-weight:600;cursor:pointer;
  box-shadow:var(--e-1)}
.dp-select:hover{background:var(--panel3)}
.dp-select:focus-visible{outline:2px solid var(--lift);outline-offset:1px}

/* Main — DPI stage list */
.ms-hero{display:flex;align-items:baseline;gap:var(--sp-4);flex-wrap:wrap;margin-bottom:var(--sp-4)}
.dpi-list{display:flex;flex-direction:column;gap:var(--sp-1)}
.dpi-row{display:flex;align-items:center;gap:var(--sp-3);
  padding:7px 10px;border-radius:var(--r-2);border:1px solid transparent}
.dpi-row.on{background:var(--panel2);border-color:var(--line2)}
.dpi-dot{width:14px;height:14px;padding:0;border-radius:50%;border:2px solid var(--line2);
  background:var(--dot,#fff);cursor:pointer;flex:none}
.dpi-row.on .dpi-dot{border-color:var(--ink)}
.dpi-n{color:var(--muted);font-size:12.5px;min-width:52px;flex:none}
.dpi-row.on .dpi-n{color:var(--ink);font-weight:600}
.dpi-in{width:92px;flex:none;background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-1);
  color:var(--ink);padding:5px 8px;font:inherit;font-size:13px}
.dpi-u{color:var(--muted2);font-size:11px;letter-spacing:1px;flex:none}
.dpi-split{color:var(--warn);font-size:11px;flex:none}
.dpi-bar{flex:1;min-width:24px;height:3px;border-radius:2px;background:var(--panel3);overflow:hidden}
.dpi-bar span{display:block;height:100%;background:linear-gradient(90deg,var(--fill-b),var(--fill-a))}
/* Per-stage color picker: a small square swatch, native input stripped of its chrome. */
.dpi-col{width:24px;height:24px;flex:none;padding:0;border:1px solid var(--line2);
  border-radius:var(--r-1);background:none;cursor:pointer}
.dpi-col::-webkit-color-swatch-wrapper{padding:2px}
.dpi-col::-webkit-color-swatch{border:none;border-radius:3px}
.dpi-rm{width:24px;height:24px;flex:none;padding:0;border:1px solid var(--line2);border-radius:var(--r-1);
  background:none;color:var(--muted);font-size:16px;line-height:1;cursor:pointer}
.dpi-rm:hover:not(:disabled){color:var(--warn);border-color:var(--warn)}
.dpi-rm:disabled{opacity:.35;cursor:default}
.dpi-actions{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-2)}
.dpi-add{background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-1);color:var(--ink);
  padding:6px 12px;font:inherit;font-size:12.5px;cursor:pointer}
.dpi-add:hover:not(:disabled){border-color:var(--ink)}
.dpi-add:disabled{opacity:.4;cursor:default}
.dpi-count{color:var(--muted2);font-size:11px;letter-spacing:1px}

/* Button Assignment — a render with a dropdown per button and a leader line to the button it
   controls. Pulsar hardcodes per-device pixel rects; the fields here are placed in percentages and
   the lines are measured at runtime (drawLeaders in js/mouse/ui.js), so the block survives a
   resize, which theirs does not have to. */
/* Centerd and width-capped so the fields sit BESIDE the render instead of out at the edges of the
   content column. They are absolutely positioned against this box, so on a wide window an
   unconstrained .ms-map parked them a long way from the mouse with the leader lines stretched
   across the gap. The cap is the field pair (2x186) plus the render plus a working gap. */
.ms-map{position:relative;min-height:430px;padding:var(--sp-4) 0;max-width:700px;margin:0 auto}
/* STACKING ORDER IS THE WHOLE TRICK, and getting it wrong is subtle rather than broken-looking.
   The lines have to sit ABOVE the render and BELOW the fields:
     art 0  <  leads 1  <  fields 2
   With the lines under the render, every one of them vanished the moment it crossed the mouse's
   opaque silhouette — so a line to the wheel or the DPI pad, both deep inside the shell, appeared
   to stop short and point at nothing, while a line to a side button on the very edge looked fine.
   It reads as bad anchor coordinates. It is not; it is z-index. */
.ms-leads{position:absolute;inset:0;pointer-events:none;z-index:1;overflow:visible}
/* Bright enough to stay legible where it crosses the shell, which is now most of its length. */
.ms-leads polyline{fill:none;stroke:var(--muted2);stroke-width:1.25}
.ms-leads circle{fill:none;stroke:var(--muted);stroke-width:1.4}
.ms-art{position:absolute;left:50%;top:0;transform:translateX(-50%);height:100%;display:grid;place-items:center;z-index:0}
.ms-svg{height:300px;width:auto}
.ms-svg path{fill:var(--key);stroke:var(--key-line);stroke-width:2}
.ms-svg line,.ms-svg rect{fill:none;stroke:var(--key-line);stroke-width:2}
/* Top-down render: taller than wide, so cap both axes or it crowds the field columns. */
.ms-render{max-height:380px;max-width:min(230px,26vw);width:auto;height:auto;display:block}

/* The field itself: icon, label, and a select that carries the value — Pulsar's shape, our skin. */
.ms-call{position:absolute;z-index:2;display:flex;align-items:center;gap:9px;
  background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-2);
  padding:7px 10px;width:186px;box-shadow:var(--e-1)}
.ms-call:hover,.ms-call:focus-within{background:var(--panel3);border-color:var(--muted2)}
.ms-call-l{left:0}
.ms-call-r{right:0}
.ms-call-ico{flex:none;display:grid;place-items:center;color:var(--muted);opacity:.9}
.ms-call:hover .ms-call-ico,.ms-call:focus-within .ms-call-ico{color:var(--ink)}
.ms-call-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px}
.ms-call-n{font-size:10px;letter-spacing:.9px;text-transform:uppercase;color:var(--muted2);
  font-family:'Red Hat Mono',monospace}
/* Native select, restyled: keeps keyboard and screen-reader behavior that a div-based menu loses. */
.ms-assign{-webkit-appearance:none;appearance:none;background:none;border:0;padding:0 14px 0 0;
  color:var(--ink);font:inherit;font-size:13px;font-weight:600;width:100%;cursor:pointer;
  text-overflow:ellipsis;
  background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),
                   linear-gradient(135deg,var(--muted) 50%,transparent 50%);
  background-position:calc(100% - 6px) center,calc(100% - 2px) center;
  background-size:4px 4px,4px 4px;background-repeat:no-repeat}
.ms-assign:focus{outline:none}
.ms-call:focus-within{outline:2px solid var(--lift);outline-offset:1px}
.ms-btn-list{display:flex;flex-direction:column;gap:var(--sp-1)}

/* Stored-macro list on the Macro page. Read-only for now — one card per button that holds a macro,
   each step a press/release with its key/button code and delay. */
.ms-mac{border:1px solid var(--line);border-radius:8px;padding:var(--sp-3);margin-top:var(--sp-3)}
.ms-mac-h{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3);flex-wrap:wrap}
.ms-mac-steps{list-style:none;margin:var(--sp-3) 0 0;padding:0;display:flex;flex-direction:column;gap:2px}
.ms-mac-steps li{display:flex;align-items:center;gap:9px;font-family:'Red Hat Mono',monospace;font-size:11px}
.ms-mac-act{flex:none;width:56px;text-transform:uppercase;letter-spacing:.9px;font-size:9.5px}
.ms-mac-press{color:var(--ok)}
.ms-mac-release{color:var(--muted2)}
.ms-mac-key{color:var(--muted)}
.ms-mac-delay{color:var(--lift)}
/* The editable step list. Same rows as the read-only one above, plus a delay field and a remove
   button, so it inherits .ms-mac-steps and only adds what differs. */
.ms-mac-edit{margin-top:var(--sp-3)}
.ms-mac-edit li{gap:8px}
.ms-mac-edit .ms-mac-key{flex:1;color:var(--ink)}
.ms-mac-d{width:72px;background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-1);
  color:var(--ink);padding:3px 6px;font:inherit;font-family:'Red Hat Mono',monospace;font-size:11px}
.ms-mac-u{color:var(--muted2);font-size:10px;letter-spacing:1px}
/* Restates its resting background AND its hover, for the reason spelled out at .mac-chip-x: a
   bare `button:hover` out-specifies a class that only set the resting state. */
.ms-mac-x{background:transparent;color:var(--muted);border:0;padding:0 4px;font-size:12px;
  line-height:1;box-shadow:none}
.ms-mac-x:hover{color:var(--err);background:transparent;filter:none}

/* Battery in the header: icon plus value, aligned on the text baseline. */
.batt{display:inline-flex;align-items:center;gap:4px;vertical-align:-2px}
.batt .icn{opacity:.8}
/* An icon inside a button sits on the text baseline, not above it. `.icn` has no display of its
   own, so without this the inline SVG rides high and pushes the button taller than its siblings. */
button .icn{display:inline-block;vertical-align:-3px;margin-right:7px}
/* FACTORY RESET IS THE SIGN AND NOTHING ELSE -- no chrome, no label. The card above it is titled
   "Factory reset" and says what it does, so a button repeating that in a pill would be the third
   time in four inches. What the sign buys instead is the half-second of hesitation the words never
   got: everybody already knows what a radiation trefoil on hazard yellow means.
   `background:none` is the shorthand on purpose -- `button:hover`'s shorthand would otherwise put
   its own color back, the same trap the effect picker's chevron fell into twice. */
.reset-sign,.reset-sign:hover{background:none;border:none;box-shadow:none;padding:4px;line-height:0}
.reset-sign{border-radius:50%;transition:filter var(--dur),transform var(--dur)}
.reset-sign:hover{filter:brightness(1.1) drop-shadow(0 0 12px rgba(245,180,0,.5))}
.reset-sign:active{transform:translateY(1px) scale(.97)}
.reset-sign:focus-visible{box-shadow:var(--ring)}
.reset-sign .icn{display:block;margin:0}

/* Below this the field pair plus the render stops fitting side by side, so both give a little. */
@media (max-width:1180px){
  .ms-map{max-width:600px}
  .ms-call{width:152px;padding:6px 9px}
  .ms-render{max-width:min(190px,22vw)}
}

/* Captured chord keys on the Buttons page's setup panel. Same row vocabulary as a macro step list,
   minus the delay field a chord has no use for. */
.ms-combo-keys li{gap:8px}
.ms-combo-keys .ms-mac-key{flex:1;color:var(--ink)}

/* Profile picker: a small dropdown in the scope bar (was three pill buttons). */
.profile-wrap{display:inline-flex;align-items:center}
.profile-sel{appearance:none;-webkit-appearance:none;font:inherit;font-size:13px;font-weight:600;
  color:var(--ink);background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-2);
  padding:6px 30px 6px 12px;cursor:pointer;line-height:1.2;
  /* chevron drawn inline so no external asset is needed */
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238b8b90' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 11px center}
.profile-sel:hover{background-color:var(--panel3);border-color:#45454d}
.profile-sel:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}
.profile-sel:disabled{opacity:.5;cursor:default}
.profile-sel.busy{opacity:.55;cursor:progress}
.profile-sel option{background:var(--panel2);color:var(--ink)}

/* "Test your settings" scratch pad on the tuning pages. */
.test-box{width:100%;box-sizing:border-box;resize:vertical;min-height:64px;
  padding:10px 12px;border-radius:var(--r-2);border:1px solid var(--line2);background:var(--panel2);
  color:var(--ink);font-family:inherit;font-size:14px;line-height:1.5}
.test-box::placeholder{color:var(--muted2)}
.test-box:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}

/* Profile buttons grayed while the board is still being read — clickable (so the toast can fire),
   just clearly not ready. */
#profile button.busy{opacity:.45;cursor:progress}

/* Soft popup: a brief, non-blocking notice pinned bottom-center. Slides up when .show is added,
   auto-hides on a timer (see toast() in app.js). Does not trap focus, unlike alert(). */
#toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,14px);z-index:1200;
  max-width:min(440px,86vw);padding:11px 18px;border-radius:var(--r-3);
  background:var(--panel3);color:var(--ink);border:1px solid var(--line2);
  box-shadow:0 10px 34px rgba(0,0,0,.5);font-size:13.5px;font-weight:500;
  opacity:0;visibility:hidden;pointer-events:none;transition:opacity .18s ease,transform .18s ease}
#toast.show{opacity:1;visibility:visible;transform:translate(-50%,0)}

/* Fn-layer highlight: hold the physical Fn key and the caps show what the Fn layer does, in the
   same category colors the keyboard's own LEDs use. Painted through `--fnhl` per key, which is a
   separate channel from the Lighting page's `--rgbp` so the two can never half-apply; while the
   highlight is up it takes the caps over completely (renderFnOverlay stops the effect preview).
   Keys with no Fn function get no `--fnhl` and fall back to the dimmed base, which is the point:
   dark means "Fn does nothing here". */
/* 3D board: .b3d-key forces background/box-shadow off by design, so the flat rule below does
   nothing there and the color comes from board3d.setKeyLight() instead -- exactly how the
   Lighting page's preview works. This clears the flat overlay quad so that light is visible. */
#board.board-3d.fn-hl .b3d-quad{background:none;box-shadow:none;border-color:transparent}

#board.fn-hl .key{background:var(--fnhl,#101013);border-color:rgba(255,255,255,.10);
  box-shadow:0 0 14px -3px var(--fnhl,transparent);transition:background .09s ease-out}
#board.fn-hl .key[style*="--fnhl"]{border-color:rgba(255,255,255,.30)}
#board.fn-hl .key-label{color:rgba(255,255,255,.92);text-shadow:0 1px 2px rgba(0,0,0,.55)}
#board.fn-hl .key-set{opacity:.5}

/* Rename control beside the profile dropdown. A `linky`, not a button: renaming is a rare,
   non-destructive aside and a third solid control in that bar would compete with the two that
   actually get used. */
.prof-rename{margin-left:8px;font-size:11px}
.prof-rename[disabled]{opacity:.4;pointer-events:none}

/* ---- effect picker (js/app.js effectPicker) ----
   A custom listbox standing in for a <select>, because a native one type-selects and there is no
   way to stop that once its popup is open. The hidden <select> is still the value's home; only
   these two elements are visible. Styled to match .profile-sel so the swap is invisible. */
.fxsel{position:relative;display:block}
.fxsel > select{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
/* THE CHEVRON IS A CUSTOM PROPERTY SO :hover CAN RESTATE IT, and it has to.
   `button:hover{background:#e8e8ea}` is the SHORTHAND, which resets background-image to none and
   background-position to 0% 0%. Its specificity (0,1,1) beats this rule's (0,1,0), so on hover the
   arrow lost its position -- and because `background` is in the button transition list, it did not
   just jump, it ANIMATED to the left edge. That is the arrow "sliding across the field".
   This is the app's standing rule (a button with its own background must restate it on :hover),
   missed here because the offender is background-POSITION rather than background-color.

   AND :hover HAS TO USE THE SHORTHAND, not a couple of longhands. The first fix restated image and
   position and left background-REPEAT behind, so `button:hover` reset it to `repeat` and the
   chevron tiled right across the button. A shorthand on our side sets every longhand at once,
   which is the only version with nothing left for the other rule to leak through. */
.fxsel-btn{--fx-chevron:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path d='M2 4.5L6 8.5L10 4.5' fill='none' stroke='%239a9aa4' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  width:100%;text-align:left;background:var(--panel2);color:var(--ink);
  border:1px solid var(--line2);border-radius:var(--r-1);padding:8px 30px 8px 11px;
  font-family:inherit;font-size:13px;font-weight:400;box-shadow:none;
  background-image:var(--fx-chevron);
  background-repeat:no-repeat;background-position:right 10px center}
.fxsel-btn:hover{background:var(--panel3) var(--fx-chevron) no-repeat right 10px center;
  border-color:#5a5a63}
/* Per-key lighting owns the board, so the effect list is inert while it is on. Still readable --
   it says what would play if you turned per-key off -- but plainly not a control right now. */
.fxsel.is-off{opacity:.45;pointer-events:none}
/* position:FIXED, and left/top/width/max-height are written by wireEffectPicker's `place()`.
   Absolute positioning inside .fxsel put this inside `.sec{overflow:hidden}` and the list was
   clipped at the card's bottom edge. Fixed takes it out of every ancestor's overflow and stacking
   context, which is the only version that stays fixed when the surrounding layout changes. */
.fxsel-list{position:fixed;z-index:60;margin:0;padding:4px;
  overflow-y:auto;list-style:none;
  background:var(--panel2);border:1px solid var(--line2);border-radius:var(--r-1);
  box-shadow:0 12px 32px rgba(0,0,0,.55)}
.fxsel-list li{padding:6px 10px;border-radius:var(--r-1);font-size:13px;color:var(--muted);cursor:pointer}
.fxsel-list li:hover{background:var(--panel3);color:var(--ink)}
.fxsel-list li.on{background:#fff;color:#000}

/* All three Firmware cards in one row: what you are running, what is available, and the reset.
   `auto-fit` rather than a fixed count because Factory reset only renders when a board is
   connected -- with a hard `repeat(3,...)` the two-card case left a visible hole. It also does the
   narrow-window collapse for free, so there is no breakpoint to keep in sync. */
.fw-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--sp-4)}
/* In a column this narrow the hero's side-by-side layout does not fit: .fw-meta's 260px minimum
   would push the card wider than its track. Stack it instead. */
.fw-grid .fw-hero{flex-direction:column;gap:var(--sp-3)}
.fw-grid .fw-meta{min-width:0;width:100%}
.fw-grid .fw-hero .big-val{font-size:34px}
