:root {
  --bg: #f3f5f9;
  --surface: #ffffff;
  --surface-2: #f8fafd;
  --ink: #101828;
  --muted: #667085;
  --faint: #98a2b3;
  --line: #e4e7ec;
  --brand: #0b2e6e;
  --accent: #2457e6;
  --accent-soft: #eaf0ff;
  --good: #12a150;
  --good-soft: #e8f7ee;
  --point: #e5484d;
  --point-soft: #fdecec;
  /* A28/Q90's third row tint. Sampled from the Nicotra catalogue's own recommended-area
     blue (#00a0e8, see charts/nomogram.js) so the table and the chart agree about what
     "recommended" looks like — and so it cannot be mistaken for the green that now means
     "meets the duty". */
  --rec-soft: #e6f5fd;
  /* A42 + A65 / Q65+Q95. The exported datasheet's own blue, `export.py`'s `BLUE`. The client
     asked for the PDF's visual system to reach the rest of the app — *"make their stylings
     with the blue bands and the colouring used in the pdfs as well … the landing and the
     search page and the admin panel all are affected"* — so the band colour is the PDF's,
     not the app's `--accent`. The two are deliberately different jobs: `--band` names a
     section, `--accent` marks the thing you are pointing at. Keep them apart. */
  --band: #006fbf;
  --band-ink: #ffffff;
  --shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 6px 20px rgba(16, 24, 40, .06);
  --shadow-hover: 0 2px 4px rgba(16, 24, 40, .06), 0 14px 34px rgba(16, 24, 40, .10);
  --radius: 14px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--ink);
  line-height: 1.5;
  font-size: 15px;
}

/* ---- Top bar ---- */
.topbar {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 16px 32px;
  background: linear-gradient(180deg, #ffffff, #fbfcff);
  /* A65: the exported sheet's masthead is a wordmark over a **blue rule**, and that rule is
     the first thing on every page. The app's header borrows it, so the two documents open
     the same way. 2px, not the 1px hairline it replaces — on paper it is a printed rule, not
     a table border. */
  border-bottom: 2px solid var(--band);
  position: sticky;
  top: 0;
  z-index: 20;
}
/* A1: all-capitals wordmark. Capitals need positive tracking where the lower-case form
   wanted negative, and the two halves are told apart by weight and colour, as in the PDF
   masthead's logo — SELEC in the base ink, TOOS in the accent. */
.brand { font-size: 20px; font-weight: 700; letter-spacing: .01em; color: var(--brand); text-decoration: none; }
.brand b { font-weight: 800; color: var(--accent); }
/* A130: the V3 logo is ~6.4:1, so its height sets its width; baseline-aligned in the bar. */
.brand-logo { height: 22px; width: auto; display: block; align-self: center; }
.login-logo { display: block; height: 26px; width: auto; margin: 0 0 14px; }
.topbar-sub { color: var(--muted); font-size: 13px; }

/* ---- Group selection page ---- */
.groups-main { max-width: 1080px; margin: 0 auto; padding: 48px 32px 64px; }
/* The card's own heading is not a band: a band names a *section*, and a card is an item in a
   grid. It takes the band colour instead, which is the other half of the PDF's system. */
.gcard h3 { color: var(--band); }

/* auto-fit, not a fixed column count: the number of product categories changes. */
.gcards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
/* A scoped account may see only one or two groups, and auto-fit would hand a lone card the
   whole 1016px row — which reads as a broken layout rather than as one card. groups.js
   caps the container to what N cards actually need (see GCARD_MAX there); the track stays
   `1fr` because the repetition count is derived from the *min* when the max is flexible,
   and a definite max silently drops the full five from three columns to two. */
.gcards { margin-inline: auto; }
@media (max-width: 880px) { .gcards { grid-template-columns: 1fr; } }

.gcard {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 26px 22px 20px;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  transition: box-shadow .2s ease, transform .2s ease;
}
.gcard:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); }
/* The whole card is one link now (flat taxonomy, no per-leaf buttons), so the anchor
   defaults have to be reset or every line renders blue and underlined. */
.gcard, .gcard:hover, .gcard:visited { text-decoration: none; color: inherit; }
.gcard-hit { position: absolute; inset: 0; z-index: 1; border-radius: var(--radius); }
.gcard-icon { color: var(--accent); margin-bottom: 10px; }
/* Real product photography when assets/groups/<id>.jpg exists; the inline SVG icon that
   follows it is the fallback and is hidden only once a photo actually loads. */
/* The mockup photos are product shots on white, at assorted aspect ratios. A fixed-height
   contain box keeps the five cards' art optically the same size without cropping any of
   them, and survives whatever the admin panel uploads later. */
/* White, not --surface-2: every catalog photo is cut out on white, so a tinted panel
   behind it just frames a paler white rectangle. */
.gcard-photo {
  display: block; width: 100%; height: 150px; object-fit: contain;
  background: #fff; border-radius: 8px; padding: 8px;
}
.gcard-photo + svg { display: none; }
.gcard-empty .gcard-icon { color: var(--faint); }
.gcard h3 { margin: 0 0 6px; font-size: 18px; }
.gcard p { margin: 0; color: var(--muted); font-size: 13.5px; flex: 1; }

/* Client branding, per the page-1 mockup: the company logo closes the landing page. */
.groups-foot { margin-top: 48px; display: flex; justify-content: center; }
.corp-logo { height: 26px; width: auto; opacity: .85; }
/* A81: below the phone breakpoint `groups.js:corpMark` serves the stacked lockup instead,
   and holding 26px there would halve its lettering — the whole point of stacking is that
   the mark gets taller once it stops having to be wide. 44px keeps the glyphs bigger than
   the one-liner's while staying inside the narrowest gutter. */
@media (max-width: 620px) { .corp-logo { height: 44px; } }

.leafbtn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  transition: border-color .15s, color .15s;
}
.leafbtn:hover { border-color: var(--accent); color: var(--accent); }
.leafbtn.muted { color: var(--faint); font-weight: 400; }
.leafrow { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 12px; }

/* ---- Group tabs (selection page) ---- */
.gtabs-bar {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  padding: 0 32px;
  position: sticky;
  top: 57px;
  z-index: 15;
}
.gtabs, .gtabs-sub { display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none; }
.gtab {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 5px;
  color: var(--muted);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
/* Page-2 mockup: each group tab carries a thumbnail of its product photo. Sized small
   enough that the five tabs still fit one row at the narrowest supported width. */
.gtab-photo {
  width: 30px; height: 30px; object-fit: contain; flex: none;
  background: #fff; border-radius: 6px; padding: 2px;
}
.gtab-label { display: inline-flex; align-items: center; gap: 6px; }
.gtab.on { color: var(--accent); border-bottom-color: var(--accent); }
.gtab:hover { color: var(--accent); }
.gtabs-sub { border-top: 1px dashed var(--line); }
.gtab.sub { font-size: 13px; padding: 7px 12px 6px; }

/* ---- Selection page layout ---- */
/* Three columns only while the results table fits beside them without a horizontal
   scrollbar — the spec'd columns (IP, ErP, the datasheet link) are the rightmost ones, so
   an overflowing table hides exactly what the user needs. Below the breakpoint the chart
   moves under the full-width table (and gets bigger for it). */
/* The results table takes the full remaining width and the chart sits under it (round-2
   page-layout mockup). The old three-column layout existed to keep the table from ever
   scrolling horizontally; at 19 columns that is no longer achievable, so the table scrolls
   with pinned identity columns instead and the chart no longer competes for width. */
.select-grid {
  display: grid;
  grid-template-areas: "side mid" "side rail";
  grid-template-columns: 300px minmax(0, 1fr);
  gap: 20px;
  padding: 20px 32px 64px;
  align-items: start;
  max-width: 1800px;
  margin: 0 auto;
}
.select-grid > .panel { grid-area: side; }
.select-grid > .mid { grid-area: mid; min-width: 0; }
/* Full width under the table. The 420 px cap and the 380 px SVG cap are gone with the
   per-row panel: the combined chart is drawn to its measured container width rather than
   at a fixed 360×300, so there is nothing left to upscale and blur. */
.select-grid > .chart-rail { grid-area: rail; min-width: 0; }

/* Below this the filter panel and the table can no longer share a row usefully, so the
   panel goes full-width above the results. */
@media (max-width: 1279px) {
  .select-grid {
    grid-template-areas: "side" "mid" "rail";
    grid-template-columns: 1fr;
    padding: 16px;
  }
  /* Stacked above the results: keep the controls at a readable measure instead of stretching
     a number input across the whole window. */
  .select-grid > .panel { max-width: 620px; }
}
@media (max-width: 1000px) {
  .rtable { font-size: 12px; }
  .rtable thead th, .rtable tbody td { padding: 8px 7px; }
}

/* ---- Filter panel ---- */
.panel {
  position: sticky;
  top: 112px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-height: calc(100vh - 130px);
  overflow-y: auto;
  padding-right: 2px;
  scrollbar-width: thin;
  scrollbar-color: #cdd5e0 transparent;
}
@media (max-width: 1279px) {
  .panel { position: static; max-height: none; }
}

.group {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  box-shadow: var(--shadow);
}
/* A42/A65: a section heading is the PDF's blue band — a solid bar the full width of the
   block, white, uppercase, tight. Same shape in the filter panel, on the datasheet page and
   in the admin panel, because in the PDF it is the same element doing the same job.
   `margin: -18px -18px 12px` pulls the bar out to the card's edges through its padding, so
   the band meets the border exactly as it meets the page margin on paper. */
.group h2 {
  margin: -18px -18px 12px;
  padding: 6px 18px;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--band);
  color: var(--band-ink);
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
}
/* Accordion filter sections (native details/summary) */
details.acc { padding: 0; }
.acc summary {
  display: flex;
  align-items: center;
  gap: 8px;
  /* A65: the band is the summary itself here — the whole row is the clickable heading, so
     giving the <h2> inside it its own bar would draw a band inside a band. */
  padding: 7px 18px;
  background: var(--band);
  color: var(--band-ink);
  cursor: pointer;
  list-style: none;
  user-select: none;
  border-radius: var(--radius);
}
details[open] > summary { border-radius: var(--radius) var(--radius) 0 0; }
.acc summary::-webkit-details-marker { display: none; }
/* Undo the card-heading band for the one inside a summary: the summary *is* the band. */
.acc summary h2 {
  margin: 0;
  flex: 1;
  padding: 0;
  background: none;
  color: inherit;
  border-radius: 0;
}
.acc summary:hover { filter: brightness(1.08); }
.acc summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.acc .chev {
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--band-ink);
  border-bottom: 2px solid var(--band-ink);
  transform: rotate(45deg);
  margin-top: -4px;
  transition: transform .15s;
}
details[open] > summary .chev { transform: rotate(225deg); margin-top: 4px; }
.acc-body { padding: 2px 18px 16px; }
/* ---- A19: the ± tolerance, against the duty value it applies to ---- */
.tolrow {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 6px;
  font-size: 12.5px;
  color: var(--muted);
}
.tolpm { font-weight: 700; color: var(--faint); }
/* ⚠️ Specificity, not preference. `.field input[type=number]` is width:100% and
   `.field.big input` is 17px with the right border stripped for the unit dropdown — both
   (0,2,1), so a bare `.tolinput` loses to them and the box swallows the whole panel with
   the "%" pushed off the edge. This selector is (0,3,1) and has to stay that way. */
.field .tolrow input.tolinput {
  flex: none;
  width: 62px;
  padding: 4px 7px;
  border: 1px solid var(--line);
  border-right: 1px solid var(--line);
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  text-align: right;
}
.tolunit { font-weight: 600; color: var(--faint); }
/* The floor note only appears when the floor actually bites (Q85), so it must not reserve
   space the rest of the time -- `hidden` plus the global [hidden] reset does that. */
.tolnote {
  margin-left: 2px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 600;
  cursor: help;
}

.acc-sub {
  font-size: 11.5px;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .07em;
  margin: 14px 0 8px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}

/* Panel tools row (global filter count + reset) */
.panel-tools {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 6px;
  min-height: 18px;
}
.panel-tools .link-btn { padding: 0; }
.fcount-line { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.fcount-line:empty { display: none; }

/* The `?` / ⓘ control. The balloon itself is `js/tooltip.js` on <body>, not a
   `::after` here — see that module for why it had to leave the panel. */
.tipicon {
  border: none;
  background: none;
  color: var(--faint);
  cursor: help;
  font-size: 12px;
  padding: 0 2px;
  font-family: inherit;
  vertical-align: baseline;
}
.tipicon:hover, .tipicon:focus { color: var(--accent); }
/* A66: the `?` variant. `ⓘ` reads as "detail you may ignore"; `?` reads as "you may not know
   what this is", which is what these are for. */
.tipicon.ask {
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  border: 1px solid var(--line);
  border-radius: 50%;
  width: 15px;
  height: 15px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
}
.tipicon.ask:hover, .tipicon.ask:focus { border-color: var(--accent); }
/* The balloon is fixed to the viewport and positioned by `js/tooltip.js`, so it is not
   inside — and cannot be clipped by — `.panel`'s or `.rtable`'s scroll container, and it
   is placed *beside* its control rather than over the field the control explains. Hidden
   with `hidden`, which `[hidden]{display:none !important}` below makes stick. */
.tipbox {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 80;
  width: max-content;
  max-width: min(300px, calc(100vw - 16px));
  white-space: normal;
  background: var(--ink);
  color: #fff;
  font-size: 12px;
  font-weight: 400;
  line-height: 1.45;
  text-align: left;
  padding: 8px 11px;
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(16, 24, 40, .22);
  pointer-events: none;
  text-transform: none;
  letter-spacing: 0;
}

.seg-note { font-size: 12px; color: var(--muted); margin-top: 6px; }

/* A65: the section badges now sit **on** the blue band, so an accent-blue pill on a blue bar
   is invisible. Inverted: white pill, band-blue digits. */
.fcount {
  display: inline-block;
  min-width: 18px;
  text-align: center;
  background: var(--band-ink);
  color: var(--band);
  font-weight: 700;
  border-radius: 999px;
  font-size: 11px;
  padding: 1px 6px;
  margin-left: 4px;
  vertical-align: 1px;
}
.fcount:empty { display: none; }

/* Author display rules (e.g. .field) outrank the UA [hidden] sheet, so make the
   hidden attribute win explicitly — filter fields self-hide when they have <2 values. */
[hidden] { display: none !important; }
.field { display: block; margin-bottom: 12px; }
.field:last-child { margin-bottom: 0; }
.field > span { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; font-weight: 500; }
.field em { font-style: normal; color: var(--faint); font-weight: 400; }

.input-wrap { position: relative; display: flex; }
.field input[type=number] {
  width: 100%;
  min-width: 0;
  padding: 9px 12px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
  transition: border-color .15s, box-shadow .15s;
}
.field.big input { font-size: 17px; font-weight: 600; padding: 11px 12px; border-radius: 10px 0 0 10px; border-right: none; }
.field input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.unit-select {
  border: 1px solid var(--line);
  border-radius: 0 10px 10px 0;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  padding: 0 8px;
  cursor: pointer;
  flex: none;
}
.unit-select:focus { outline: none; border-color: var(--accent); color: var(--accent); }

/* segmented control */
.seg { display: flex; background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px; padding: 3px; gap: 3px; }
.seg button {
  flex: 1;
  padding: 7px 0;
  border: none;
  background: transparent;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--muted);
  cursor: pointer;
  font-family: inherit;
  transition: background .15s, color .15s, box-shadow .15s;
}
.seg button.on { background: var(--surface); color: var(--accent); box-shadow: 0 1px 3px rgba(16,24,40,.12); }

/* sliders */
.field.slider b { color: var(--accent); font-weight: 700; }
.field.slider input { width: 100%; accent-color: var(--accent); cursor: pointer; }

/* chips */
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  transition: all .15s;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.on { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
/* Single-value filter: shown for completeness, nothing to choose between. */
.chip.static { cursor: default; background: var(--surface-2); color: var(--faint); }
.chip.static:hover { border-color: var(--line); color: var(--faint); }
/* Listed by the client, no catalog behind it yet — still selectable, visibly empty. */
.chip.muted { color: var(--faint); border-style: dashed; }
.chip.muted.on { color: inherit; border-style: solid; }
.fieldnote { display: block; margin-top: 5px; font-size: 11.5px; color: var(--faint); }

/* A83: Part Number — a searchable multi-select. The list floats over the panel below it
   rather than pushing the filters down while someone types. */
.parts { position: relative; }
.parts input[type=search] {
  width: 100%; min-width: 0; box-sizing: border-box;
  padding: 9px 12px; background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; color: var(--ink); font-size: 14px; font-family: inherit;
}
.parts input[type=search]:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.parts-list {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20;
  max-height: 260px; overflow-y: auto; background: var(--surface);
  border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 8px 24px rgba(16, 24, 40, .14); padding: 4px;
}
.parts-opt {
  display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 7px;
  font-size: 13px; color: var(--ink); cursor: pointer;
}
.parts-opt:hover { background: var(--surface-2); }
.parts-opt input { accent-color: var(--accent); margin: 0; flex: none; }
.parts-none { padding: 8px; font-size: 12px; color: var(--faint); }
.parts-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.parts-chips:empty { display: none; }
.parts-chip {
  display: inline-flex; align-items: center; gap: 4px; padding: 4px 4px 4px 10px;
  border-radius: 999px; background: var(--accent-soft); color: var(--accent);
  font-size: 12px; font-weight: 600; max-width: 100%;
}
.parts-chip button {
  border: none; background: transparent; color: inherit; cursor: pointer;
  font-size: 14px; line-height: 1; padding: 0 5px; border-radius: 999px;
}
.parts-chip button:hover { background: var(--surface); }

.range-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.range-row .field { min-width: 0; }
.range-row .field input { font-size: 13px; }

.link-btn {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  padding: 0 0 12px;
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
}
.link-btn:hover { text-decoration: underline; }

/* ---- Results head ---- */
.results-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 9px; /* + the 3 px .load-bar track below = the original 12 px gap */
  min-height: 30px;
}
.summary { color: var(--muted); font-size: 14px; }
.summary b { color: var(--ink); font-size: 16px; }

.cards { transition: opacity .18s ease; }
.cards.loading { opacity: .55; pointer-events: none; }

/* ---- Loading a search ---- */
/* The track is always in the layout (transparent, 3 px) so switching the indicator on
   never shifts the table down. Only searches that outlast LOAD_GRACE_MS light it up —
   with live search most responses land in tens of milliseconds, and an indicator that
   appeared for each of those would strobe on every keystroke. */
.load-bar {
  height: 3px;
  border-radius: 999px;
  overflow: hidden;
  background: transparent;
  transition: background .15s ease;
}
.load-bar.on { background: var(--accent-soft); }
.load-bar.on::after {
  content: "";
  display: block;
  height: 100%;
  width: 32%;
  border-radius: inherit;
  background: var(--accent);
  /* -100% parks it just off the left edge; 312% carries its trailing edge off the right
     (the element is 32 % of the track, so one track-width is 312 % of its own width). */
  animation: load-sweep 1.1s cubic-bezier(.65, .05, .35, .95) infinite;
}
@keyframes load-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(312%); }
}

/* Skeleton rows, for the case dimming cannot cover: an empty results area (the first
   search in a group, or straight after a group switch) has nothing to dim. Deliberately
   NOT dimmed alongside — see showLoad(): a faded placeholder is no placeholder. */
.skel { padding: 14px 16px; }
.skel-head, .skel-row {
  border-radius: 8px;
  /* Tuned against --surface (white), not --surface-2: a placeholder a shade off the card
     it sits on is indistinguishable from an empty card. */
  background: linear-gradient(90deg, #e9edf3 25%, #f6f8fc 37%, #e9edf3 63%);
  background-size: 400% 100%;
  animation: skel-shimmer 1.3s ease-in-out infinite;
}
.skel-head { height: 20px; width: 34%; margin-bottom: 12px; }
.skel-row { height: 34px; }
.skel-row + .skel-row { margin-top: 8px; }
/* Later rows lag, so the block reads as one surface filling in rather than six bars
   pulsing in lockstep. */
.skel-row:nth-child(2) { animation-delay: .08s; opacity: .9; }
.skel-row:nth-child(3) { animation-delay: .16s; opacity: .8; }
.skel-row:nth-child(4) { animation-delay: .24s; opacity: .7; }
.skel-row:nth-child(5) { animation-delay: .32s; opacity: .6; }
.skel-row:nth-child(6) { animation-delay: .40s; opacity: .5; }
@keyframes skel-shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

.head-right { display: flex; align-items: center; gap: 12px; flex: none; }
.table-tools { display: flex; align-items: center; gap: 12px; }
.table-tools:empty { display: none; }
.rowcount { font-size: 12.5px; color: var(--muted); font-weight: 600; }

/* ---- Column / value popovers ---- */
.colmenu {
  position: absolute;
  z-index: 40;
  min-width: 190px;
  max-height: 340px;
  overflow-y: auto;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 12px 32px rgba(16, 24, 40, .18);
  font-size: 13px;
}
.colmenu-head {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 2px 6px 6px;
}
.colmenu label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px;
  border-radius: 6px;
  cursor: pointer;
}
.colmenu label:hover { background: var(--accent-soft); }
.colmenu input { accent-color: var(--accent); }
.colmenu-foot { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 6px; }
.colmenu-foot button {
  width: 100%;
  border: none;
  background: none;
  padding: 5px 6px;
  border-radius: 6px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent);
  text-align: left;
  cursor: pointer;
}
.colmenu-foot button:hover { background: var(--accent-soft); }
/* Two buttons side by side once the picker gained "Reset to default" (A23). */
.colmenu-foot { display: flex; gap: 4px; }
.colmenu-foot button { width: auto; flex: 1; }

/* A32: the numeric half of a heading's filter. Label above, box below, so a long column
   name does not squeeze the input to nothing at the menu's min-width. */
.colmenu-num {
  display: block;
  padding: 5px 6px;
  font-size: 12.5px;
  color: var(--muted);
  cursor: default;
}
.colmenu-num:hover { background: none; }
.colmenu-num input {
  display: block;
  width: 100%;
  margin-top: 3px;
  padding: 5px 7px;
  border: 1px solid var(--line);
  border-radius: 6px;
  font: inherit;
  font-size: 13px;
}
.colmenu-note {
  padding: 6px;
  margin-top: 4px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 11.5px;
  line-height: 1.4;
}
.colmenu-unit { color: var(--faint); font-weight: 600; }
.colmenu-hint { padding: 0 6px 6px; font-size: 11.5px; color: var(--faint); }

/* A23: drag-to-reorder rows in the Columns picker. The handle is the affordance; the whole
   row is draggable so a shaky grab still works. */
.colmenu-list { display: flex; flex-direction: column; }
.colmenu-row { cursor: grab; }
.colmenu-row.dragging { opacity: .45; cursor: grabbing; }
.draghandle { color: var(--faint); font-size: 12px; letter-spacing: -1px; cursor: grab; }
.colmenu-label { flex: 1; }

/* A32 count line: "10 found, 4 shown (2 column limits)". */
.rowcount-why { font-weight: 500; color: var(--faint); }
/* A28: the shortfall count, in the summary line beside the match count. */
.short-note { color: #a2262b; font-weight: 700; cursor: help; }
/* Installation facts on the count line: the fan split and the site air. Muted
   rather than coloured — they qualify the answer, they are not a warning about it,
   which is what .short-note's red means. */
.site-note { color: var(--muted, #6a7280); cursor: help; }
/* A heading that names its column but cannot be sorted by it — it must read as a
   heading, not as a dead control, so it takes the type without the hover affordance. */
.colhead-plain { cursor: default; }

/* Excel-style filter affordance, on every filterable heading since A32. */
.colfilter {
  border: none;
  background: none;
  margin-left: 4px;
  padding: 0 2px;
  color: var(--faint);
  font-size: 10px;
  cursor: pointer;
}
.colfilter:hover { color: var(--accent); }
.colfilter.on { color: var(--accent); }
.colfilter.on::after { content: "●"; font-size: 7px; vertical-align: super; }

/* ---- Results table ---- */
.table-scroll {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
/* max-content so 19 nowrap columns get the width they need, min-width so a short column
   set still fills the container instead of hugging the left edge. */
.rtable { width: max-content; min-width: 100%; border-collapse: collapse; font-size: 13.5px; }
.rtable thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface-2);
  color: var(--muted);
  font-size: 11.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  text-align: left;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
}
.rtable thead th small { text-transform: none; color: var(--faint); font-weight: 500; }
/* A25: the unit is its own line under the label and deliberately *larger* than the 11.5 px
   uppercase heading — it is the thing a reader checks a number against, and as a run-on
   <small> it was the least legible text in the table. `vertical-align: top` on the cell
   keeps a one-line header (Model, Qty) aligned with the two-line ones beside it. */
.rtable thead th { vertical-align: top; }
.rtable thead th .colunit {
  display: block;
  margin-top: 2px;
  text-transform: none;
  letter-spacing: 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--faint);
}
.rtable thead th:hover { color: var(--accent); }
.rtable thead th[aria-sort=ascending], .rtable thead th[aria-sort=descending] { color: var(--accent); }
.rtable tbody td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
  white-space: nowrap;
}
/* ---- Pinned identity columns ----
   The motorized set is 19 columns and cannot fit any screen, so the table scrolls and the
   columns that say *which fan a row is* are pinned instead: thumbnail + model at the left
   edge, the datasheet link at the right. Backgrounds must be opaque and restated per row
   state, because a sticky cell sits over the scrolling ones. The rec-in/rec-out row
   gradients are replaced by flat fills here — a gradient would restart inside each pinned
   cell and not line up with the row behind it. */
.rtable .pin { position: sticky; z-index: 1; background: var(--surface); }
.rtable thead th.pin { z-index: 3; background: var(--surface-2); }
.rtable .c-thumb { left: 0; width: 58px; min-width: 58px; padding-left: 12px; padding-right: 12px; }
.rtable .c-model { left: 58px; min-width: 200px; box-shadow: 8px 0 8px -8px rgba(16, 24, 40, .18); }
.rtable .c-ds { right: 0; text-align: right; box-shadow: -8px 0 8px -8px rgba(16, 24, 40, .18); }
.rtable tbody tr:hover td.pin { background: var(--surface-2); }
.rtable tbody tr.duty-meets td.pin { background: var(--good-soft); }
.rtable tbody tr.duty-short td.pin { background: var(--point-soft); }
.rtable tbody tr.rec-in td.pin { background: var(--good-soft); }
.rtable tbody tr.rec-out td.pin { background: var(--rec-soft); }

.rtable .c-qty { text-align: center; }
.qty { color: var(--muted); }

/* Real product photo, with the fan-type line drawing behind it. The <img> removes itself
   on 404 (no assets ship yet), which un-hides the SVG through this sibling rule. */
.fanthumb { width: 34px; height: 34px; object-fit: contain; display: block; }
.fanthumb + svg { display: none; }

/* Narrow desktops: tighten the cells so fewer columns are scrolled out of reach. */
@media (max-width: 1360px) {
  .rtable { font-size: 12.5px; }
  .rtable thead th, .rtable tbody td { padding: 9px 8px; }
  .material { max-width: 96px; }
  .rtable .c-thumb { width: 50px; min-width: 50px; padding-left: 8px; padding-right: 8px; }
  .rtable .c-model { left: 50px; min-width: 170px; }
}
.rtable tbody tr:last-child td { border-bottom: none; }
.rtable tbody tr { cursor: pointer; transition: background .12s; }
.rtable tbody tr:hover { background: var(--surface-2); }
/* Flat tints, not the left-fading gradients round 1 used: a pinned cell has to restate the
   row's background to sit opaquely over the scrolling ones, and a gradient restarts inside
   each cell — the pinned datasheet column would show a detached block of colour that does
   not line up with the row behind it. */
/* A28/Q90, A91/Q135: one tint per state. Green is the common case — a fan that delivers
   what was asked, and inside the recommended area where the catalogue draws one; blue is a
   Nicotra-style fan that delivers it outside that area; red is the exception the client wants
   to spot while scrolling, a fan that cannot reach the duty at all. */
.rtable tbody tr.duty-meets { background: var(--good-soft); }
.rtable tbody tr.duty-short { background: var(--point-soft); }
/* A121 (img 6): the Version cell of a size row — one button per version. A150 (23 Sep): the
   red drew the eye to every size row in the table, so it is no longer the resting state.
   `.needs` is a quiet dashed outline; `.warn` (duty-short's palette on the cell, never the
   row) appears only when the user tries to open the row without choosing. */
.vpick { display: flex; flex-wrap: nowrap; gap: 4px; padding: 3px; border-radius: 7px; }
.vpick.needs { box-shadow: inset 0 0 0 1px var(--line); }
.vpick.warn { background: var(--point-soft); box-shadow: inset 0 0 0 1.5px var(--point); }
.vnote {
  position: absolute; z-index: 40; max-width: 280px; padding: 8px 11px; border-radius: 8px;
  background: var(--ink); color: var(--surface); font-size: 12.5px; line-height: 1.4;
  box-shadow: 0 6px 20px rgba(16, 24, 40, .22);
}
.vbtn {
  font: inherit; font-size: 12px; font-weight: 600; min-width: 30px; padding: 3px 7px;
  border: 1px solid var(--line); border-radius: 6px; background: var(--surface);
  color: var(--ink); cursor: pointer;
}
.vbtn:hover { border-color: var(--accent); color: var(--accent); }
.vbtn.on { background: var(--accent); border-color: var(--accent); color: #fff; }
/* A149: a version that exists but cannot run at this duty — listed, never choosable. */
.vbtn.off, .vbtn.off:hover {
  background: none; border-style: dashed; color: var(--faint); border-color: var(--line);
  text-decoration: line-through; cursor: not-allowed; opacity: .75;
}
.rtable tbody tr.needs-version { cursor: default; }
.dslink.off { opacity: .45; cursor: not-allowed; }
.rtable tbody tr.rec-in { background: var(--good-soft); }
.rtable tbody tr.rec-out { background: var(--rec-soft); }
.rtable td small, .rtable .modelcell small { display: block; color: var(--faint); font-size: 10.5px; }

.c-thumb { width: 44px; padding-right: 0 !important; }
.thumb { color: var(--accent); display: block; }
/* Two lines: model, then series + match/recommended badges. Keeping series and the badges
   on their own line halves this column's width, which is what lets the table fit. */
.modelcell b { font-size: 14px; display: block; }
.modelcell .mtags { display: block; margin-top: 2px; line-height: 1.5; }
.modelcell .mseries { font-size: 11px; color: var(--faint); font-weight: 500; }
.modelcell .mseries + .dutytag, .modelcell .mseries + .rectag { margin-left: 6px; }
/* A28: replaces `.matchtag`, the percentage badge. Only the shortfall is named — "meets the
   duty" is the normal case and a badge on every row is noise. */
.dutytag {
  margin-left: 0;
  font-size: 10.5px;
  font-weight: 700;
  border-radius: 999px;
  padding: 1px 7px;
  cursor: help;
}
.dutytag.short { color: #a2262b; background: #fbdcdd; }
.rectag { margin-left: 0; font-size: 10.5px; font-weight: 700; border-radius: 999px; padding: 1px 7px; }
.rectag.in { color: #0a5c85; background: var(--rec-soft); }
.rectag.out { color: var(--muted); background: var(--surface-2); }
.material { max-width: 120px; overflow: hidden; text-overflow: ellipsis; display: inline-block; vertical-align: middle; }
/* An empty cell is a statement about the datasheet, not a glitch — the tooltip says which. */
.nodata { color: var(--faint); cursor: help; border-bottom: 1px dotted var(--line); }
/* Value clamped at the edge of a published curve family: a bound, not a reading. */
.bounded { cursor: help; border-bottom: 1px dotted var(--faint); }
.erptag.yes { color: var(--good); font-weight: 700; }
.erptag.no { color: var(--faint); }
.erptag.bad { color: #a2262b; font-weight: 700; }
.erptag.na { color: var(--faint); }
.dslink { color: var(--accent); font-weight: 600; text-decoration: none; font-size: 12.5px; }
.dslink:hover { text-decoration: underline; }

/* ---- Chart panel ---- */
.chart-panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px;
}
.chart-panel:empty { display: none; }
.chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
.chart-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.scale-seg button { padding: 4px 10px; font-size: 12px; }
.chart-model { font-size: 16px; font-weight: 800; letter-spacing: -.01em; }
.chart-sub { color: var(--muted); font-size: 12.5px; }
.ds-btn {
  flex: none;
  padding: 7px 12px;
  background: var(--accent);
  color: #fff;
  border-radius: 9px;
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.ds-btn:hover { filter: brightness(1.1); }
.ds-btn.off { opacity: .45; cursor: not-allowed; }

/* ---- Combined performance chart ---- */
.cc-host {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 4px 4px;
}
.cc-svg { display: block; width: 100%; }
.cc-hit { cursor: pointer; }
.cc-line, .cc-area { transition: stroke-width .12s ease, opacity .12s ease; pointer-events: none; }
.chart-empty { padding: 32px; text-align: center; color: var(--muted); font-size: 13px; }

.cc-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 10px;
  margin-top: 10px;
}
.cc-key {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 3px 10px 3px 7px;
  background: none;
  font: inherit;
  font-size: 12px;
  color: var(--muted);
  /* A153 (23 Sep) reverses A34/Q92: a click on a key pins that fan — its curve and its row
     stay highlighted, and "View datasheet" opens it — until the key is clicked again. */
  cursor: pointer;
}
.cc-key.pinned { border-color: var(--accent); color: var(--ink); font-weight: 600; }
.cc-key .cc-sup { color: var(--faint); font-weight: 500; }
.cc-key:hover { background: var(--surface-2); color: var(--ink); }
.cc-key.on { border-color: var(--line); background: var(--surface-2); color: var(--ink); font-weight: 600; }
.cc-key.static { cursor: default; }
.cc-key.static:hover { background: none; color: var(--muted); }
.cc-key i { width: 9px; height: 9px; border-radius: 50%; flex: none; }
/* A64: the plot area invites a click — it sets the duty point. */
.cc-svg .cc-capture { cursor: crosshair; }
/* A41: the system-curve key is a dashed rule, not a dot — it names a line on the chart. */
.cc-key i.dashed {
  width: 16px; height: 0; border-radius: 0; background: none !important;
  border-top: 1.4px dashed var(--point);
}

/* Row highlighted from the chart side. Distinct from :hover so hovering a curve visibly
   marks its row even though the pointer is nowhere near the table. */
.rtable tbody tr.hot { background: var(--surface-2); }
.rtable tbody tr.hot td.pin { background: var(--surface-2); }
/* A153: the row pinned from the chart legend. A ring drawn on the cells (the pinned cells
   repaint their own background, so a ring on <tr> would disappear behind them), and never a
   background: the green / blue / red state tint underneath must stay readable — the purple
   "selected" fill was removed (A120) for hiding exactly that. */
.rtable tbody tr.pinned td {
  box-shadow: inset 0 2px 0 var(--accent), inset 0 -2px 0 var(--accent);
}
.rtable tbody tr.pinned td:first-child {
  box-shadow: inset 2px 0 0 var(--accent), inset 0 2px 0 var(--accent), inset 0 -2px 0 var(--accent);
}
.rtable tbody tr.pinned td:last-child {
  box-shadow: inset -2px 0 0 var(--accent), inset 0 2px 0 var(--accent), inset 0 -2px 0 var(--accent);
}

.chart {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px;
}
.chart svg { width: 100%; height: auto; display: block; }
.chart .cap, .cap { font-size: 11px; color: var(--faint); text-align: center; margin-top: 6px; }
.chart .legend, .legend { display: flex; justify-content: center; flex-wrap: wrap; gap: 12px; margin-top: 6px; font-size: 11px; color: var(--muted); }
.legend i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }

.opline {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
  padding: 10px 14px;
  margin-top: 12px;
  background: var(--good-soft);
  border: 1px solid #cdebd8;
  border-radius: 10px;
}
.opline .stat span { display: block; font-size: 11px; color: #3d7a55; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
.opline .stat b { font-size: 14px; color: #0d5c33; }

/* ---- Empty states ---- */
.empty {
  color: var(--muted);
  padding: 48px 24px;
  text-align: center;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.empty button {
  margin-top: 14px;
  padding: 9px 18px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 9px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
}
.empty button:hover { filter: brightness(1.1); }
.error { color: var(--point); }

/* ---- Datasheet page ---- */
.ds-main { max-width: 1100px; margin: 0 auto; padding: 24px 32px 64px; }
.ds-head { margin-bottom: 18px; }
.ds-title { display: flex; align-items: center; gap: 16px; margin-top: 10px; }
.ds-thumb {
  color: var(--accent);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 10px;
  box-shadow: var(--shadow);
}
/* The extracted product image replaces the generic type icon only once it has loaded;
   a missing asset removes the <img> and the icon behind it shows through. */
.ds-thumb img { display: block; width: 56px; height: 56px; object-fit: contain; }
.ds-thumb img + svg { display: none; }
.ds-drawing img {
  display: block;
  width: 100%;
  max-width: 760px;
  margin: 4px auto 8px;
  border-radius: 8px;
}
/* ---- A95: the datasheet page laid out as the PDF (datasheet.sheetLayout) ----
   One white sheet, the PDF's blue bands, and the same two-column head: inputs, fan and
   photo on the left, the chart in the wider right column. Collapses to one column on a
   narrow screen, where a side-by-side chart would be unreadable. */
.ds-sheet {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px 20px 20px;
}
.ds-band {
  margin: 0 0 8px; padding: 4px 8px; background: var(--band); color: var(--band-ink);
  font-size: 12px; font-weight: 700; letter-spacing: .03em;
}
/* Case is data here: `a1` and `øD` are dimension names from the catalogue drawing, and the
   table style's uppercasing turns them into `A1` and `ØD`. The band titles arrive in capitals
   from export.py already, with their units (`mm`) in the case the PDF prints. */
.ds-sheet .rtable thead th { text-transform: none; letter-spacing: 0; }
.ds-top { display: grid; grid-template-columns: minmax(0, 43fr) minmax(0, 57fr); gap: 20px; }
.ds-left .ds-band + .ds-pairs { margin-bottom: 14px; }
.ds-pairs {
  display: grid; grid-template-columns: minmax(110px, auto) 1fr; gap: 3px 14px;
  font-size: 13px; align-items: baseline;
}
.ds-pairs .k { font-weight: 700; color: var(--ink); }
.ds-pairs .v { color: var(--ink); }
.ds-pairs .u, .ds-results .u { color: var(--muted); font-size: 11.5px; }
.ds-pairs2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; }
.ds-photo { display: block; max-width: 100%; max-height: 190px; margin: 8px auto 0; object-fit: contain; }
.ds-right .ds-toggles { margin: 2px 0 6px; }
.ds-resrow { margin-top: 18px; }
.ds-resrow.with-sketch { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 36%); gap: 20px; align-items: start; }
.ds-sketch { display: block; width: 100%; max-height: 260px; object-fit: contain; margin-top: 30px; }
.ds-results { width: 100%; border-collapse: collapse; font-size: 13px; }
.ds-results th { text-align: left; font-weight: 700; padding: 3px 10px 3px 0; white-space: nowrap; vertical-align: top; }
.ds-results td { padding: 3px 6px; vertical-align: top; }
.ds-results .sym { text-align: center; color: var(--ink); }
.ds-results .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ds-results .note { color: var(--muted); font-size: 12px; }
.ds-block { margin-top: 18px; }
.ds-legendline { margin: 4px 0 0; font-size: 11.5px; color: var(--muted); }
.ds-notes { margin: 0; padding-left: 18px; font-size: 12.5px; color: var(--muted); }
.ds-notes li { margin: 3px 0; }
.ds-more { margin-top: 18px; }
.ds-more > summary { cursor: pointer; font-weight: 650; color: var(--accent); padding: 6px 0; }
.ds-more .ds-section { margin-top: 12px; }
@media (max-width: 900px) {
  .ds-top, .ds-resrow.with-sketch, .ds-pairs2 { grid-template-columns: minmax(0, 1fr); }
  .ds-sketch { margin-top: 0; }
}

/* Download row. Both downloads are the same control visually — the Selectoos sheet is a
   <button> only because it has to POST the rasterized chart. */
.ds-downloads { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.dl-btn {
  display: inline-block;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 7px 14px;
  cursor: pointer;
}
.dl-btn:hover:not(:disabled) { border-color: var(--accent); background: var(--surface-2); }
.dl-btn:disabled { color: var(--muted); cursor: not-allowed; opacity: .6; }
.dl-note, .dl-status { font-size: 12.5px; color: var(--muted); }
.dl-status:not(:empty) { color: var(--point); }

.ds-title h1 { margin: 0; font-size: 24px; letter-spacing: -.01em; }
.ds-sub { color: var(--muted); font-size: 13.5px; }
.ds-desc { color: var(--muted); margin: 0 0 18px; max-width: 720px; }
.ds-section { margin-bottom: 16px; }
.ds-chart { max-width: 780px; }
.ds-toggles { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 10px; font-size: 13px; color: var(--muted); }
.ds-toggles input { accent-color: var(--accent); }
/* Catalog palette legend: power and efficiency are both black on the RLO sheet, so the
   swatches are what tells the families apart. */
.nomo-legend { display: flex; gap: 16px; flex-wrap: wrap; margin: 8px 0 2px; font-size: 12.5px; color: var(--muted); }
.nomo-key { display: inline-flex; align-items: center; gap: 6px; }
.nomo-key svg { flex: none; }
/* A43: one chart, not a grid of small multiples. The grid rule is kept as a single column
   so the surrounding markup and the Nicotra sheet's own layout are untouched. */
.ds-curves { display: grid; grid-template-columns: 1fr; gap: 14px; }
.ds-curve svg { cursor: crosshair; }  /* A35: the plot reads out under the pointer */
.curve-sub { font-weight: 500; color: var(--faint); margin-left: 6px; }
.ds-curve { background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px; padding: 10px; }
/* A125/A126: one chart, no header and no emphasis — the matched class survives only as the
   export's hook (datasheet.js wireDownloads), so it carries no style. */

/* A42 / Q65: the same content in the same order under the same section names as the exported
   sheet, laid out for a screen. The PDF's label/value pairs are separated by a fine rule the
   full width of each column, which is what makes a long block scannable on paper; the screen
   version borrows it. Nothing is removed to look more like paper — the chart toggles, the
   download buttons and the scrolling tables all stay (Q65 is explicit about that). */
.specs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 24px; font-size: 13px; }
@media (max-width: 880px) { .specs { grid-template-columns: 1fr 1fr; } }
.specs > div {
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}
.specs .k { display: block; color: var(--faint); font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em; }
.specs .v { font-weight: 650; color: var(--ink); }
/* Secondary detail under a spec value — the orderable code beneath a build name, a build's
   speed ceiling, the "derived" tag on an efficiency. Must break to its own line: run inline
   it reads as part of the value ("11.0 kW · 160L / IE3-6build ceiling 1,405 1/min"). */
.specs small { display: block; font-weight: 500; color: var(--faint); font-size: 11px; }
.ds-note { margin: 12px 0 0; font-size: 12.5px; color: var(--muted); max-width: 720px; }
.ds-section h2 .rectag { margin-left: 8px; vertical-align: 2px; text-transform: none; letter-spacing: 0; }

.rtable.dims { font-size: 12.5px; }
.rtable.dims thead th { cursor: default; }
.rtable.dims tbody tr { cursor: default; }
.rtable.dims tbody tr:hover { background: transparent; }

@media (prefers-reduced-motion: reduce) {
  .cards { transition: none; }
  .gcard, .rtable tbody tr { transition: none; }
  /* Both indicators stay — presence is what says "busy" — but stop travelling. The bar
     fills its track instead of sweeping it, and the skeleton holds one flat tone. */
  .load-bar.on::after { width: 100%; animation: none; }
  .skel-head, .skel-row { background: #e9edf3; animation: none; }
}

/* ---- Admin panel ---- */
/* Uses the :root tokens throughout; no new colours except a warning amber, which the
   palette had no equivalent of. */
:root {
  --warn: #b54708;
  --warn-soft: #fff6ed;
}

.adm-main { padding: 24px; }
.adm-wrap { max-width: 1180px; margin: 0 auto; display: grid; gap: 18px; }

.adm-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
}
.adm-head h1 { margin: 0; font-size: 24px; color: var(--brand); }
.adm-sub { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
.adm-headtools { display: flex; gap: 8px; }

.adm-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 18px 20px;
}
/* A65/Q95: *"the landing and the search page and the admin panel all are affected."* Same
   band, same proportions as the customer app and as the exported sheet. */
.adm-card h2 {
  margin: -18px -20px 12px; padding: 6px 20px;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--band); color: var(--band-ink);
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  display: flex; align-items: center; gap: 8px;
}
.adm-count {
  background: var(--band-ink); border: 1px solid transparent; border-radius: 999px;
  padding: 1px 9px; font-size: 11px; color: var(--band); font-weight: 700;
}

/* A8: the operator guide.
   The level is a fixed left gutter rather than a chip after each heading, matching the PDF:
   the client asked for actions "marked by the level they need", which only works as a column
   the eye can run down. It collapses above the action on a narrow screen — a 96 px gutter
   plus body text does not fit a phone, and stacking keeps the pairing without a scrollbar. */
.gd-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 20px;
  flex-wrap: wrap;
}
.gd-intro { flex: 1 1 420px; max-width: 62ch; margin: 0; }
/* A command is typed, so it is selectable text in a box, not prose. `pre` keeps the two-line
   backup pair as two lines and the wrapped continuation of the recovery command intact. */
.gd-cmd {
  margin: 6px 0 0; padding: 7px 10px; overflow-x: auto;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; color: var(--ink); white-space: pre;
}
.gd-item {
  display: grid; grid-template-columns: 96px 1fr; gap: 12px;
  padding: 10px 0; border-top: 1px solid var(--line);
}
.gd-item:first-of-type { border-top: 0; }
.gd-level {
  font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em;
  padding-top: 2px;
}
.gd-item h3 { margin: 0 0 3px; font-size: 14px; }
.gd-item p { margin: 0; font-size: 13px; color: var(--muted); max-width: 78ch; }
@media (max-width: 620px) {
  .gd-item { grid-template-columns: 1fr; gap: 2px; }
}

/* stats */
.adm-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.adm-stat {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
}
.adm-statval { font-size: 26px; font-weight: 600; color: var(--brand); line-height: 1.1; }
.adm-statlabel { font-size: 13px; font-weight: 500; margin-top: 2px; }
.adm-statnote { font-size: 11px; color: var(--faint); }

/* buttons */
.adm-btn {
  font: inherit; font-size: 13px; padding: 8px 14px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  cursor: pointer; text-decoration: none; display: inline-flex; align-items: center;
}
.adm-btn:hover { background: var(--surface-2); }
.adm-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.adm-btn.primary:hover { filter: brightness(1.06); }
.adm-btn.ghost { color: var(--muted); }
.adm-btn.tiny { padding: 4px 9px; font-size: 12px; }
.adm-btn.danger { color: var(--point); border-color: #f3c9ca; }
.adm-btn.danger:hover { background: var(--point-soft); }
.adm-btn:disabled { opacity: .6; cursor: default; }

/* notices */
.adm-notice {
  border-radius: 10px; padding: 10px 14px; font-size: 13px; border: 1px solid transparent;
}
.adm-notice.ok { background: var(--good-soft); border-color: #bfe6cd; color: #0b6b36; }
.adm-notice.error { background: var(--point-soft); border-color: #f3c9ca; color: #a3282c; }

.adm-warn { border-color: #f3d9be; background: var(--warn-soft); }
/* A65: `.adm-card h2` now paints a blue band, and this rule only recoloured the *text* — so
   the warning heading became amber on blue and read as a rendering fault. A warning band is
   an amber band; it is still a band, which is what keeps the panel's grammar consistent. */
.adm-warn h2 { background: var(--warn); color: #fff; }
.adm-reasons { margin: 8px 0 0; padding-left: 18px; font-size: 13px; }
.adm-reasons li { margin: 3px 0; }
.adm-warntext { color: var(--warn); font-size: 12px; }
/* A111: a twin-fan unit, badged in the warning tone rather than the grey of every other note. */
.adm-twin {
  display: inline-block; padding: 2px 8px; border-radius: 999px; font-weight: 700;
  background: var(--warn); color: #fff; font-size: 11.5px; cursor: help;
}
.adm-twinline { color: var(--warn); }

/* upload */
.adm-drop {
  margin-top: 12px; border: 2px dashed var(--line); border-radius: var(--radius);
  padding: 28px; text-align: center; cursor: pointer; background: var(--surface-2);
  transition: border-color .12s, background .12s;
}
.adm-drop:hover, .adm-drop:focus-visible, .adm-drop.over {
  border-color: var(--accent); background: var(--accent-soft); outline: none;
}
.adm-drop.busy { opacity: .55; pointer-events: none; }
.adm-dropinner b { color: var(--accent); }
.adm-dropinner small { display: block; color: var(--faint); font-size: 12px; margin-top: 4px; }
.adm-progress { margin-top: 12px; }
/* The load bar is always laid out at 3px in the results page; here it only exists while
   an upload is running, so it needs its own track. */
.adm-progress .load-bar { position: relative; height: 3px; }

/* tables */
.adm-tablewrap { overflow-x: auto; margin-top: 10px; }
.adm-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.adm-table th {
  text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--faint); font-weight: 600; padding: 6px 10px; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.adm-table td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
.adm-table tbody tr:hover { background: var(--surface-2); }
.adm-table small { color: var(--faint); font-size: 11px; }
.adm-rowhidden td { opacity: .6; }
.adm-rowactions { white-space: nowrap; text-align: right; }
.adm-rowactions .adm-btn + .adm-btn { margin-left: 6px; }

.adm-status {
  font-size: 11px; padding: 2px 8px; border-radius: 999px; text-transform: capitalize;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--muted);
}
.adm-status.published { background: var(--good-soft); border-color: #bfe6cd; color: #0b6b36; }
.adm-status.draft { background: var(--accent-soft); border-color: #cfdcff; color: var(--accent); }

.adm-flag {
  display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 999px;
  background: var(--warn-soft); border: 1px solid #f3d9be; color: var(--warn);
}

/* What the extractor read off a staged datasheet: three labelled counts whose meaning
   depends on the document shape, plus the calibration residual where the family has one.
   Deliberately plain text rather than pills — these are neutral facts, and the amber and
   dashed pills either side of them in the same row mean "something is wrong". The numbers
   are bolded because the labels are only there to say which number is which. */
.rev-counts { white-space: normal; min-width: 160px; }
.rev-count {
  display: inline-block; font-size: 11px; color: var(--muted); white-space: nowrap;
  margin-right: 8px;
}
.rev-count b { color: var(--text); font-variant-numeric: tabular-nums; }

/* A63. The same pill as .adm-flag but in its own class: a flag is the extractor's QA note
   about this document, and an unfilled field is a job for a person. They sit in the same cell
   and must not read as one list. Muted rather than amber — it warns and never blocks, and a
   second alarm colour beside "not searchable" would make the weaker claim look like the
   stronger one. The field names are on `title`; the cell only has room for the count. */
.adm-gaps {
  display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 999px;
  background: var(--surface-2); border: 1px dashed var(--line); color: var(--muted);
  cursor: help; white-space: nowrap;
}
.adm-notice.warn { background: var(--warn-soft); border-color: #f3d9be; color: var(--warn); }
/* Two columns, because this list is usually a dozen short labels and one per line turns the
   drawer into a scroll. `columns` rather than a grid: the items are plain text of uneven
   length and nothing needs to line up across the gutter. */
.adm-emptylist {
  margin: 8px 0 0; padding-left: 20px; columns: 2; column-gap: 24px;
  font-size: 13px; color: var(--muted);
}
.adm-emptylist li { break-inside: avoid; }

.adm-review { border-color: var(--accent); }
.adm-actions { display: flex; gap: 8px; margin-top: 14px; }
.rev-note { font-size: 12px; }

/* sign in */
.adm-login { display: grid; place-items: center; min-height: 72vh; padding: 24px; }
.adm-loginbox { width: 100%; max-width: 360px; display: grid; gap: 12px; }
.adm-loginbox h1 { margin: 0; font-size: 20px; color: var(--brand); }
.adm-loginbox .field input {
  width: 100%; font: inherit; padding: 9px 11px; border: 1px solid var(--line);
  border-radius: 9px; background: var(--surface);
}
.adm-loginbox .field input:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
.adm-loginerr {
  background: var(--point-soft); border: 1px solid #f3c9ca; color: #a3282c;
  border-radius: 9px; padding: 8px 11px; font-size: 13px;
}
/* S4/S6: a neutral message on the sign-in screen — "a code is on its way", "your password
   has been changed". Deliberately not `.adm-loginerr`'s red: these are outcomes, and half of
   them are successes. Same geometry so the box does not jump as one replaces the other. */
.adm-loginnote {
  background: var(--surface-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 9px; padding: 8px 11px; font-size: 13px;
}
.adm-back { font-size: 12px; color: var(--muted); text-decoration: none; text-align: center; }
.adm-back:hover { color: var(--accent); }

@media (max-width: 900px) {
  .adm-stats { grid-template-columns: repeat(2, 1fr); }
}

/* ---------------------------------------------------------------- signed-in chrome
   The topbar is a baseline-aligned flex row with no spacer, so the user chip claims the
   right edge with margin-left:auto rather than by reordering anything. */
.topbar-user {
  margin-left: auto;
  display: flex;
  align-items: baseline;
  gap: 12px;
  font-size: 13px;
}
.topbar-who { color: var(--ink); font-weight: 600; }
.topbar-link {
  font: inherit; font-size: 13px; color: var(--muted);
  background: none; border: 0; padding: 0; cursor: pointer; text-decoration: none;
}
.topbar-link:hover { color: var(--accent); text-decoration: underline; }

/* ---------------------------------------------------------------- admin forms
   Before accounts arrived, `.adm-loginbox .field input` was the only styled input in the
   panel and it was scoped to the sign-in card — so a form anywhere else rendered with the
   browser's own controls. These rules are that scope widened, plus the checkbox grid the
   group pickers need. */
.adm-form { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); }
.adm-form > summary {
  cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent);
  list-style: none; padding: 4px 0;
}
.adm-form > summary::-webkit-details-marker { display: none; }
.adm-form > summary::before { content: "+\00a0"; font-weight: 700; }
.adm-form[open] > summary::before { content: "\2212\00a0"; }

.adm-fieldrow { display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0; }
.adm-fieldrow .field { flex: 1 1 180px; }
.adm-scales > div { flex: 1 1 260px; }
.adm-scale { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 4px 0; }
.adm-scale span { flex: 1; }

.adm-form .field input,
.adm-form .field select {
  width: 100%; font: inherit; padding: 9px 11px; border: 1px solid var(--line);
  border-radius: 9px; background: var(--surface);
}
.adm-form .field input:focus,
.adm-form .field select:focus {
  outline: 2px solid var(--accent-soft); border-color: var(--accent);
}

.adm-checks { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 6px 0; }
.adm-check {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--ink); white-space: nowrap; cursor: pointer;
}
.adm-check input { margin: 0; cursor: pointer; }
.adm-check input:disabled + span { color: var(--faint); }

.adm-hint { margin: 0; font-size: 12px; color: var(--muted); }
.adm-swap {
  font: inherit; font-size: 12px; color: var(--accent);
  background: none; border: 0; padding: 4px 0; cursor: pointer; text-align: center;
}
.adm-swap:hover { text-decoration: underline; }

/* The accounts table's group pickers make rows much taller than the product table's, so
   its action cell must not stretch with them. */
.adm-table .adm-rowactions { vertical-align: top; }

/* ---- admin: section tabs + the activity log -------------------------------------- */

.adm-nav { display: flex; gap: 4px; margin: 0 0 18px; border-bottom: 1px solid var(--line); }
.adm-tab {
  padding: 9px 14px; font-size: 13px; font-weight: 600; color: var(--muted);
  text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.adm-tab:hover { color: var(--ink); }
.adm-tab.on { color: var(--accent); border-bottom-color: var(--accent); }

/* The action buttons sit OUTSIDE the auto-fit grid, in a flex row beside it. Inside it
   they became a track of their own, sized like a filter field — too narrow for two
   buttons, so "Clear" wrapped onto the label row and stopped lining up with the inputs. */
.adm-filterbar {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; margin: 14px 0 4px;
}
/* Auto-fit rather than fixed columns: six controls that must not push each other off the
   card at a narrow width. */
.adm-filters {
  flex: 1 1 620px;
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  align-items: end;
}
.adm-fil { display: flex; flex-direction: column; gap: 4px; }
.adm-fil > span { font-size: 11px; font-weight: 600; color: var(--muted); }
.adm-fil input, .adm-fil select {
  font: inherit; font-size: 13px; padding: 7px 9px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--surface); width: 100%;
}
.adm-filacts { display: flex; gap: 6px; align-items: flex-end; }
.adm-filacts .adm-btn { white-space: nowrap; }

/* ---- A12: an activity row is one line ----
   `filterSummary` stringifies a whole filter object into one cell and a product id can be
   long, so both used to wrap and a page of log rows became a page of paragraphs. They are
   clipped instead, with the full text on the element's `title` — the log is scanned, and
   what is worth reading in full is worth reading in a tooltip rather than in a cell three
   lines tall. `max-width` is what makes `text-overflow` work at all: without it the cell
   sizes to its content and there is nothing to overflow. */
.adm-activity td { white-space: nowrap; vertical-align: middle; }
.adm-activity td.adm-clip {
  max-width: 220px; overflow: hidden; text-overflow: ellipsis;
}
.adm-activity td.adm-clip.wide { max-width: 300px; }

.adm-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 12px; flex-wrap: wrap;
}
.adm-pager .adm-btn + .adm-btn { margin-left: 6px; }

/* A zero-result search is the row an admin is scanning for, so it reads as a word in the
   alert colour rather than as a 0 among the other counts. */
.adm-zero { color: var(--point); }
.adm-qid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--faint); }

/* ------------------------------------------------------------------ review + spec editing

   The review report and the spec form are the two things in the panel that need more room
   than a row, so they open over it. There was no dialog anywhere in this stylesheet before;
   these rules are that scope widened, and they stay inside the existing token and button
   vocabulary rather than introducing a second one. */

.adm-status.pass { background: var(--good-soft); border-color: #bfe6cd; color: #0b6b36; }
.adm-status.warn { background: var(--warn-soft); border-color: #f3d9be; color: var(--warn); }
.adm-status.fail { background: var(--point-soft); border-color: #f3c8c8; color: var(--point); }
.adm-status.skip,
.adm-status.unknown { background: var(--surface-2); color: var(--muted); }
.adm-status.info { background: var(--accent-soft); border-color: #cfdcff; color: var(--accent); }

.adm-alias { color: var(--faint); }

.adm-checks-list { list-style: none; margin: 10px 0; padding: 0; font-size: 13px; }
.adm-checks-list li { padding: 4px 0; border-bottom: 1px solid var(--line); }
.adm-checks-list li:last-child { border-bottom: 0; }
.adm-checks-list small { color: var(--muted); }

/* ---- A11: the image replacement pair in the edit drawer ----
   `.missing` rather than removing the <img>: the slot has to stay, because "there is no
   picture here" is exactly the state an admin opens this section to fix. */
.adm-imgrow { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 10px; }
.adm-imgcell {
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
  width: 160px;
}
.adm-imglabel { font-size: 11px; font-weight: 600; color: var(--muted); }
.adm-imgcell img {
  width: 160px; height: 120px; object-fit: contain;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
}
/* An empty framed box reads as "nothing here yet". The dashed border is the same language
   the empty-results panel uses. */
.adm-imgcell img.missing { border-style: dashed; }
.adm-imgcell label.adm-btn { cursor: pointer; }

/* ---- A7: the confirm dialog ----
   Its own namespace, because `.adm-modal` below is the *drawer's* backdrop and a second
   meaning of that class would inherit its layout. z-index 60 puts it above the drawer,
   which matters for the one case that stacks: closing a drawer with unsaved edits. */
.adm-ask-host { position: fixed; inset: 0; z-index: 60; }
.adm-ask-back { position: absolute; inset: 0; background: rgba(16, 24, 40, .5); }
.adm-ask {
  position: relative;
  margin: 12vh auto 0;
  width: calc(100% - 32px);
  max-width: 460px;
  max-height: 76vh;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 20px 22px 18px;
}
.adm-ask h3 { margin: 0 0 8px; font-size: 16px; line-height: 1.35; }
.adm-ask p { margin: 0 0 12px; color: var(--muted); font-size: 13px; line-height: 1.5; }
/* Pre-wrap: a publish-over-failing-review dialog lists one product per line, and that list
   is the whole reason this dialog exists rather than a one-line question. */
.adm-ask-detail {
  white-space: pre-wrap; font-size: 12.5px; line-height: 1.5; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
  padding: 10px 12px; margin-bottom: 12px; max-height: 30vh; overflow: auto;
}
.adm-ask .field { margin-bottom: 10px; }
.adm-ask-err { color: var(--point); font-size: 12.5px; margin-bottom: 10px; }
.adm-ask-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
@media (prefers-reduced-motion: no-preference) {
  .adm-ask { animation: adm-ask-in .12s ease-out; }
}
@keyframes adm-ask-in { from { transform: translateY(-6px); opacity: 0 } to { transform: none; opacity: 1 } }

.adm-modal {
  position: fixed; inset: 0; z-index: 50; display: flex; padding: 24px;
  align-items: flex-start; justify-content: center; background: rgba(0, 0, 0, .45);
  overflow: auto;
}
.adm-modalbox {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 18px 20px 20px; width: 100%; max-width: 780px;
  max-height: calc(100vh - 48px); overflow: auto;
}
.adm-modalbox.wide { max-width: 1040px; }
/* The review report is a whole page of someone else's design: its own stylesheet is
   max-width 1500px and its crop-beside-values grid collapses below 1050px, so a narrower
   box does not shrink the comparison, it stacks it. */
.adm-modalbox.full { max-width: 1500px; }
.adm-modalhead {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-bottom: 6px;
}
.adm-modalhead h2 { margin: 0; font-size: 17px; }

/* The report is a whole document of its own; it gets a viewport, not a height that grows
   with its content — the drawer already scrolls and two nested scrollers read as broken. */
.adm-modalframe {
  width: 100%; height: 72vh; border: 1px solid var(--line); border-radius: 6px;
  background: #fff; margin-top: 10px; display: block;
}

/* The workspace: the report is no longer something to glance at but the surface being edited,
   so the box takes the viewport and the frame takes what the chrome above and below it does
   not. The 1500 px cap stays — the report's own stylesheet is 1500 px wide and its
   crop-beside-values grid collapses below 1050 px, and with the editor *inside* the frame
   nothing competes with it for width. */
.adm-modalbox.workspace {
  height: calc(100vh - 48px); display: flex; flex-direction: column; overflow: hidden;
}
/* The frame takes the room, and the sections under it scroll *inside themselves* rather than
   pushing it out of the box: opening the nominal table must not shrink the page the values
   are being checked against down to a letterbox. */
.adm-modalbox.workspace .adm-framewrap { flex: 1 1 300px; min-height: 300px; display: flex; }
.adm-modalbox.workspace .adm-modalframe { height: auto; flex: 1 1 auto; }
.adm-modalbox.workspace #adm-drawer-extra { flex: 0 1 auto; overflow: auto; min-height: 0; }
.adm-modalbox.workspace .adm-framewrap .empty { flex: 1 1 auto; margin-top: 10px; }

/* Denser than .adm-fieldrow's flex row: a spec form is fifteen short fields, and a wrapping
   flex line leaves ragged gaps at every width. */
.adm-fieldgrid {
  display: grid; gap: 10px 14px; margin-top: 12px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.adm-fieldgrid .field input {
  width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 6px;
  font: inherit; font-size: 13px; background: var(--surface);
}
.adm-fieldgrid .field input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.adm-fieldgrid .field em {
  display: block; margin-top: 3px; font-size: 11px; font-style: normal; color: var(--muted);
}
.adm-modalbox .adm-table input {
  width: 100%; min-width: 78px; padding: 4px 6px; border: 1px solid var(--line);
  border-radius: 4px; font: inherit; font-size: 12px;
}

.adm-raw {
  width: 100%; margin-top: 8px; padding: 10px; border: 1px solid var(--line);
  border-radius: 6px; background: var(--surface-2); resize: vertical;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
  line-height: 1.45;
}

/* The spec form is fifteen fields plus a nominal table, so on any real product Save sits
   below the fold of the drawer's own scroller. Pinning the action row to the bottom of the
   box keeps the primary action reachable without scrolling past a four-row table — the
   drawer scrolls under it. In the workspace it matters more, not less: the thing being
   edited is a full-height report, and Save must not be somewhere below it. */
.adm-modalbox > #adm-drawer-actions {
  position: sticky; bottom: -20px; z-index: 2; margin-top: 16px;
  background: var(--surface); border-top: 1px solid var(--line);
}
.adm-modalbox > #adm-drawer-actions:empty { border-top: 0; }
.adm-modalbox > #adm-drawer-actions .adm-actions { margin-top: 0; padding: 12px 0; }
.adm-modalbox.workspace > #adm-drawer-actions { flex: none; }
.adm-modalbox.workspace > #adm-drawer-status { flex: none; }

/* The status region carries the verdict, the failing checks and the save result. Its list is
   capped rather than allowed to push the report off the screen: on a failing document it can
   run to a dozen rows, and the page they are about is the point. */
.adm-modalbox.workspace .adm-checks-list { max-height: 22vh; overflow: auto; }

/* A13/A14 — the Groups & brands section.
   `.tx-card` is a `.adm-form` that is a block rather than a disclosure, so it needs a border
   of its own: `.adm-form`'s only separator is its top rule, and five of these stacked read
   as one long form rather than as five groups. */
.tx-card {
  border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px;
  margin-top: 14px; background: var(--surface);
}

.tx-card code { font-size: 11px; color: var(--muted); }

/* The poles control is the one three-valued family, so it is a select sitting inside a row
   of checkboxes. Bounded, or it stretches to the width of the flex row. */
.tx-poles { flex: 0 0 auto; min-width: 160px; }

.tx-poles select,
.tx-card input[type="text"] {
  font: inherit; padding: 7px 10px; border: 1px solid var(--line);
  border-radius: 9px; background: var(--surface);
}

.tx-card input[type="text"] { width: 100%; }

/* A5 — the role selector in the accounts table. Sized to its content so the Role column
   does not widen the whole table, and dimmed when the last-Owner rail disables it. */
.adm-rolesel {
  font: inherit; font-size: 12px; padding: 5px 8px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--surface); color: var(--ink);
}

.adm-rolesel:disabled { color: var(--faint); cursor: not-allowed; }

.adm-table .tx-blabel { width: 100%; max-width: 220px; }

/* A69: the per-fan point table. */
.adm-fieldrow .field.pt-fanfield { flex: 3 1 360px; }
.pt-select {
  width: 100%; padding: 9px 10px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: var(--ink); font: inherit; font-size: 14px;
}
.pt-select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.pt-table td, .pt-table th { text-align: right; white-space: nowrap; }
.pt-table td:first-child, .pt-table th:first-child { text-align: left; color: var(--muted); }

/* ---- A150 (23 Sep): versions on the datasheet ----
   The picker under the title, and the choice list in the dialog a chart click can open.
   The picker reuses the results table's `.vbtn` so a version looks the same on both pages. */
.ds-versions { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 6px; }
.ds-versions > span { font-size: 12.5px; color: var(--muted); margin-right: 3px; }
.dlg-choices { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 14px; }
.dlg-choice {
  display: flex; align-items: center; gap: 9px; padding: 7px 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; font-size: 13.5px;
}
.dlg-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.dlg-choice small { color: var(--muted); }
