/* The Atech module catalog. Uses tokens.css only — no colours invented here, so
   the catalog looks like the storefront in both themes.

   Re-skinned 2026-08-24 to the Atech-UI (Mitchell) recipes: pill buttons/chips,
   card/tile/input radii from components.css's scale (24/16/12/pill), Geist Mono
   micro-labels, glass toolbar. Values are written out here (not consumed as
   components.css classes) because this file styles markup that modules.js emits —
   renaming those classes would mean editing the emitter. Layout mechanics —
   the container query, sticky/--navh/z-index contract, focus rings — are
   untouched; only the look changed. */

/* A QUERY CONTAINER, not a media breakpoint. This component has three hosts of
   very different widths — /modules (1140px of content at 1440), /bench (704px)
   and the app's #catalogdrawer, measured 2026-08-20 at 450px of content inside
   the phase-pane stage — and all three can be on screen at the same window
   size. `@media` asks the wrong question here: what matters is how wide THIS
   pane is, never how wide the window is.

   Nothing else in the file needed one until the sourcing control grew a third
   option (.mc-seg 244px -> 372px), which pushed line 1 past the drawer's width:
   measured scrollWidth 502 against clientWidth 450 before the rule below.

   Safe as containment: `container-type: inline-size` also applies
   `contain: layout`, which makes .mc a containing block for absolutely
   positioned descendants — checked, and every absolute in this file
   (.mc-searchicon, .mc-count, .mc-badge2, .mc-badgerec, .mc-3dnote, .mc-no3d)
   already resolves against a nearer `position: relative` wrapper
   (.mc-searchwrap, .mc-thumb, .mc-3d/.mc-photo), so none of them moves. */
.mc { padding: 18px 20px 4px; container-type: inline-size; }

.mc-head { display: flex; justify-content: space-between; align-items: flex-end;
  gap: 16px; margin-bottom: 16px; }
.mc-head h3 { margin: 0 0 4px; font-size: 17px; letter-spacing: -0.01em;
  color: var(--text-primary); }
.mc-head p { margin: 0; font-size: 12px; }

.mc-dim { color: var(--text-muted); }
.mc-pad { padding: 20px; font-size: 13px; }

.mc-fail { margin: 18px 20px; padding: 14px 16px; border-radius: 16px;
  background: var(--state-crit-bg); color: var(--state-crit);
  display: flex; flex-direction: column; gap: 5px; font-size: 12.5px; }
.mc-fail b { font-size: 13px; }

/* -- the toolbar: search, tier, categories -------------------------------- */

/* Sticky, so the fix for an empty result is always one press away without a
   scroll back up — the empty state never replaces this. The negative margin +
   matching padding lets the background reach the panel's edges while the
   controls stay aligned with the grid underneath.

   `top: var(--navh)`, not `top: 0`. MEASURED 2026-08-20 on /modules at
   scrollY=700: this toolbar and the page header both stuck to 0, and with this
   at z-index 6 against #top's 5 the 1180px toolbar drew straight OVER the
   1440px header — three of the four nav links returned .mc-bar from
   elementFromPoint at their own centres. /bench had the same collision with the
   z-indexes inverted (#top at 20), so the toolbar slid invisibly UNDER the
   header instead. Both shells now measure their own header into --navh with a
   ResizeObserver and #top outranks this at z-index 30.

   The 0 FALLBACK IS LOAD-BEARING: the app's catalog drawer (index.html's
   #catalogmount) has no page header at all and never defines --navh, so this
   must keep sticking to the top of the drawer there. */
.mc-bar { position: sticky; top: var(--navh, 0px); z-index: 6;
  display: flex; flex-direction: column;
  gap: 8px; margin: 0 -20px 12px; padding: 10px 20px 10px;
  /* Glass, like the page headers above it (2026-08-24): a solid bar under a
     translucent header read as two different products stacked. Same top/z-index
     machinery as before — only the surface changed. */
  background: var(--glass-bg-strong);
  backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
  border-bottom: 1px solid var(--border-light); }

/* LINE 1 shares one row wherever it fits: the search field flexes and the
   segmented control does not, so the search absorbs every pixel of change.
   Re-measured 2026-08-20 with three sourcing options: still one row on
   /modules and /bench at every window width from 640 to 1882 (search 749px
   down to 209px, .mc-search's own min-width is 120px).

   The old comment claimed one row "at any width", which stopped being true the
   moment .mc-seg went from 244px to 372px: in the app's catalog drawer, 450px
   of content, that leaves 68px for a field whose minimum is 120 — measured
   scrollWidth 502 vs clientWidth 450, i.e. the control ran off the edge of the
   drawer. So the rule is now conditional and the condition is the PANE, not the
   window. Below it, line 1 becomes two rows and the segmented control takes the
   second one full-width; --mc-barh is re-measured by the ResizeObserver in
   _mountShell, so every sticky offset keyed on it follows without a hardcoded
   number. */
.mc-barrow { display: flex; align-items: center; gap: 10px; flex-wrap: nowrap; }

.mc-searchwrap { position: relative; flex: 1 1 auto; min-width: 120px;
  display: flex; align-items: center; }
.mc-searchicon { position: absolute; left: 10px; width: 14px; height: 14px;
  color: var(--text-muted); pointer-events: none; }
.mc-search { font: inherit; font-size: 12.5px; width: 100%; height: 34px;
  padding: 0 46px 0 30px; border-radius: 12px;
  background: var(--input-bg); color: var(--input-text);
  border: 1px solid var(--input-border);
  transition: background .2s cubic-bezier(0.16, 1, 0.3, 1),
              border-color .2s cubic-bezier(0.16, 1, 0.3, 1),
              box-shadow .2s cubic-bezier(0.16, 1, 0.3, 1); }
.mc-search:focus { outline: none; border-color: var(--input-focus-ring);
  background: var(--surface-primary); box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.04); }
.dark .mc-search:focus { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.06); }
.mc-search::placeholder { color: var(--input-placeholder); }
/* The browser's own clear affordance duplicates Esc and steals width from a
   field that is already the tightest thing in the row. */
.mc-search::-webkit-search-cancel-button { display: none; }

/* Inside the field, always present. A badge that DISAPPEARS at zero reads as a
   broken box; "0" is the answer to the question that was asked. */
.mc-count { position: absolute; right: 8px; min-width: 22px; text-align: center;
  padding: 2px 6px; border-radius: 999px; font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  font-size: 10px; line-height: 1.4; pointer-events: none;
  background: var(--surface-secondary); color: var(--text-muted); }
.mc-count.is-zero { background: var(--state-warn-bg); color: var(--state-warn); }

/* The tier switch. THREE buttons since 2026-08-20 — the comment that stood here
   said "EXACTLY two … see SOURCING in modules.js for why a third is not an
   option", and that argument is now dead: the served default (atech_preferred)
   was being DISPLAYED as one of the other two, so the control named a scope the
   project did not have. SOURCING carries the full story.

   flex:none, so it keeps its measured width and the search field absorbs every
   pixel of change instead. Measured after the third button landed, 1440x900:
   .mc-seg 244px -> 371px, and .mc-barrow still fits on one row on /modules
   (1140px), /bench (704px) and in the app's catalog drawer. See .mc-barrow
   above for why one row is load-bearing. */
.mc-seg { display: flex; gap: 2px; padding: 3px; flex: 0 0 auto;
  background: var(--state-idle-bg); border: 1px solid var(--border-light);
  border-radius: 12px; }
.mc-segbtn { font: inherit; font-size: 11.5px; line-height: 1.4; cursor: pointer;
  display: flex; align-items: center; gap: 6px; white-space: nowrap;
  padding: 6px 10px; border-radius: 9px; border: 1px solid transparent;
  background: transparent; color: var(--text-secondary); }
.mc-segbtn:hover { color: var(--text-primary); }
/* Active scope = the dark pill of the tab-pills recipe (was a white card tab). */
.mc-segbtn.on { background: var(--button-primary-bg); color: var(--button-primary-text);
  border-color: transparent; font-weight: 600;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); }
.mc-segbtn:focus-visible { outline: 2px solid var(--input-focus-ring); outline-offset: 1px; }
.mc-segn { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 10px;
  padding: 1px 5px; border-radius: 999px; background: var(--surface-secondary);
  color: var(--text-muted); }
/* A count that could not be derived writes '' (see SOURCING's count() in
   modules.js) and must then take NO space at all: an empty pill beside a label
   reads as a number that failed to load, which is a claim about the catalog
   this component is not entitled to make. Unknown is absent here, not zero. */
.mc-segn:empty { display: none; }

/* NARROW PANE — line 1 becomes two rows.
   540px of container = the 502px line 1 needs at its tightest (372px of
   segments + 10px gap + .mc-search's 120px minimum) plus air. Measured in the
   app's catalog drawer, not guessed, and asked of the PANE rather than the
   window: at a 1440px viewport the drawer is 490px wide while /modules beside
   it is 1180px, so one @media breakpoint could only ever be right for one of
   them.
   (Re-measured 2026-08-24 after the Atech-UI pill re-skin widened .mc-seg to
   383px: tightest line 1 is now 513px, still inside the 540 with air; one row
   holds on /modules — search 747px — and /bench — search 310px.)

   POSITION IS LOAD-BEARING. This block sat above `.mc-seg` on the first
   attempt and did nothing: `@container` adds no specificity, so the later
   `.mc-seg { flex: 0 0 auto }` won the cascade and the control still measured
   372px inside a 290px pane. It has to come after every rule it overrides.
   (Measured after moving it: drawer overflow 82px -> 0.) */
@container (max-width: 540px) {
  .mc-barrow { flex-wrap: wrap; row-gap: 8px; }
  .mc-searchwrap { flex: 1 1 100%; }
  /* Full width and equal shares, so the three scopes read as one control
     rather than as a huddle pushed against the right edge. `flex-wrap` on the
     group itself is the last resort for a truly narrow drawer: the buttons
     stack onto a second line inside the pill rather than any of them — or any
     of their counts — being clipped or dropped. */
  .mc-seg { flex: 1 1 100%; flex-wrap: wrap; }
  .mc-segbtn { flex: 1 1 auto; justify-content: center; }
}
/* The count pill inside the active (dark) scope pill: tint from the pill's own
   text colour so it works in both modes (the pill is black-on-light and
   white-on-dark). */
.mc-segbtn.on .mc-segn {
  background: color-mix(in srgb, var(--button-primary-text) 16%, transparent);
  color: var(--button-primary-text); }

/* LINE 2. Wraps freely — unlike line 1, a chip row that becomes two rows is
   still perfectly usable, and the --mc-barh measurement follows it. */
.mc-chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.mc-chip2 { font: inherit; font-size: 11px; line-height: 1.4; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 4px 9px; border-radius: 999px;
  background: var(--card-bg); color: var(--text-secondary);
  border: 1px solid var(--card-border); }
.mc-chip2:hover { border-color: var(--border-medium); color: var(--text-primary); }
.mc-chip2.on { background: var(--foreground); color: var(--background);
  border-color: var(--foreground); font-weight: 600; }
.mc-chip2:focus-visible { outline: 2px solid var(--input-focus-ring); outline-offset: 1px; }
.mc-chipn { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 9.5px; opacity: .75; }

/* The bar's last line: the "what each scope means" trigger, and the caller's
   caption for the tier switch (the standalone page uses it to say the choice is
   not being saved). Inside the toolbar, so --mc-barh measures it. */
.mc-barfoot { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
.mc-srcnote { font-size: 10.5px; line-height: 1.4; color: var(--text-muted); }

/* The disclosure trigger NAMES WHAT IS INSIDE IT — "What ‘Atech only’, ‘Atech
   first’ and ‘Any hardware’ mean", built from SOURCING so it keeps naming them
   as the row grows — rather than a caret or the word "details". A trigger whose
   label does not say what opening it produces is a control the reader has to
   press to find out, which is not a disclosure, it is a guess. */
.mc-seghelp { font: inherit; font-size: 10.5px; line-height: 1.4; cursor: pointer;
  padding: 0; border: 0; background: transparent; color: var(--text-secondary);
  text-decoration: underline; text-underline-offset: 2px;
  text-decoration-color: var(--border-medium); }
.mc-seghelp:hover { color: var(--text-primary); }
.mc-seghelp:focus-visible { outline: 2px solid var(--input-focus-ring); outline-offset: 2px; }
.mc-seghelp::after { content: ' ▾'; font-size: 8px; vertical-align: 1px; }
.mc-seghelp[aria-expanded="true"]::after { content: ' ▴'; }

/* The panel. Below the sticky bar, never inside it: open, this is ~90px of
   prose, and a toolbar that grows to 190px eats the grid it exists to filter.
   [hidden] costs nothing at all. */
.mc-segwhat { display: flex; flex-direction: column; gap: 8px;
  margin: 0 0 12px; padding: 12px 14px; border-radius: 16px;
  background: var(--surface-secondary); border: 1px solid var(--border-light); }
.mc-segwhat[hidden] { display: none; }
.mc-segwhat-row { display: flex; flex-direction: column; gap: 2px; }
.mc-segwhat-row b { font-size: 12px; color: var(--text-primary); }
.mc-segwhat-row span { font-size: 11.5px; line-height: 1.5; color: var(--text-secondary);
  max-width: 68ch; }

.mc-aside:empty { display: none; }

/* -- empty states --------------------------------------------------------- */

/* Never a bare "no results": each of the four cases names what was searched and
   offers the one press that fixes it. Sits UNDER the toolbar, which stays
   mounted, so the fix never requires scrolling back up. */
.mc-empty { display: flex; flex-direction: column; gap: 6px; padding: 22px 18px;
  margin-bottom: 18px; border-radius: 16px; text-align: left;
  background: var(--surface-secondary); border: 1px dashed var(--border-medium); }
.mc-empty[hidden] { display: none; }
.mc-empty b { font-size: 13px; color: var(--text-primary); }
.mc-empty span { font-size: 11.5px; line-height: 1.6; color: var(--text-secondary);
  max-width: 62ch; }
.mc-empty .mc-dim { font-size: 10.5px; }
.mc-emptyacts { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 4px; }
.mc-eact { font: inherit; font-size: 11.5px; font-weight: 600; padding: 6px 13px;
  border-radius: 999px;
  cursor: pointer; background: var(--button-secondary-bg);
  color: var(--button-secondary-text); border: 1px solid var(--button-secondary-border); }
.mc-eact:hover { border-color: var(--border-dark); color: var(--text-primary); }
.mc-eact.is-go { background: var(--button-primary-bg); color: var(--button-primary-text);
  border-color: var(--button-primary-bg); }
.mc-eact.is-go:hover { background: var(--button-primary-bg-hover); }

.mc-kitnote { padding-bottom: 16px; }
.mc-kitnote:empty { display: none; }

/* The same fact, restated inside a kit part's own detail panel. It is not a
   duplicate of `.mc-kitnote`: that one only exists while a kit part happens to
   be in the grid, and a reader who deep-linked to #m-<ky-part> never sees it.
   Since these parts render no per-part buy link (the 37 shared one URL — see
   _buyLink), this is the only purchase route the panel has. */
.mc-kitbuy { margin: 0; font-size: 11.5px; line-height: 1.6;
  color: var(--text-secondary); max-width: 68ch; }
.mc-kitbuy .ot-ds { margin-top: 0; white-space: nowrap; }

/* -- what I already have --------------------------------------------------- */

/* Below the toolbar, not inside it: both this and the stored-alias note are
   occasional, and neither should push search and the tier switch down the
   screen on every visit. The disclosure auto-opens on the raw stored mode
   `specific_hardware`. */
.mc-aside { margin-bottom: 12px; }
.mc-owned { display: none; flex-direction: column; gap: 5px; margin-top: 10px; }
.mc-owned.on { display: flex; }
.mc-owned label { font-size: 11.5px; color: var(--text-secondary); }
.mc-owned textarea { font: inherit; font-size: 12.5px; line-height: 1.55;
  padding: 9px 11px; border-radius: 12px; resize: vertical; min-height: 62px;
  background: var(--input-bg); color: var(--input-text);
  border: 1px solid var(--input-border); }
.mc-owned textarea:focus { outline: none; border-color: var(--input-focus-ring); }
.mc-owned textarea::placeholder { color: var(--input-placeholder); }
.mc-ownednote { font-size: 10.5px; line-height: 1.5; color: var(--text-muted); }

/* -- sourcing ------------------------------------------------------------- */

.mc-src { margin-bottom: 14px; }
.mc-srclabel { font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted); margin-bottom: 8px; font-family: 'Geist Mono', ui-monospace, Menlo, monospace; }
.mc-srcopts { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 8px; }
.mc-opt { text-align: left; padding: 12px 14px; border-radius: 16px; cursor: pointer;
  background: var(--card-bg); border: 1px solid var(--card-border);
  display: flex; flex-direction: column; gap: 4px; font: inherit;
  transition: border-color .15s ease, background .15s ease; }
.mc-opt:hover { border-color: var(--border-medium); }
.mc-opt b { font-size: 13px; color: var(--text-primary); }
.mc-opt span { font-size: 11.5px; line-height: 1.45; color: var(--text-secondary); }
.mc-opt.on { border-color: var(--border-dark); background: var(--surface-secondary); }
.mc-opt.on b::after { content: ' ✓'; color: var(--state-ok); }

/* The rounding note. It used to be on screen for EVERY visitor — `atech_preferred`
   was the served default and was aliased onto "Atech only", so this sentence was
   the only thing correcting a control that named the wrong scope. Since
   2026-08-20 the control offers `atech_preferred` itself and this fires for one
   stored mode only, `specific_hardware`, which genuinely is not a scope a grid
   can list. Measured on /modules, /bench and in the app drawer after the change:
   no .mc-srcalias in the document under the served default. */
.mc-srcalias { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; }
.mc-srcalias code { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 10.5px; }

/* -- non-Atech hardware, shown when sourcing permits it -------------------- */

/* Quieter than the module cards on purpose: these parts clip into no port, get
   no "add to board" action, and equal-weight tiles would imply they behave the
   same way. Text-first — most have no picture worth showing, and an empty image
   well reads as a broken card. */
.ot-block { margin: 18px 0 0; }
.ot-head { display: flex; align-items: baseline; gap: 9px; margin: 0 0 9px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-secondary); font-family: 'Geist Mono', ui-monospace, Menlo, monospace; }
.ot-head span { font-size: 10px; font-weight: 400; letter-spacing: 0;
  text-transform: none; color: var(--text-muted); }
.ot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(164px, 1fr));
  gap: 8px; }
.ot-card { display: flex; flex-direction: column; gap: 2px; padding: 9px 11px;
  border-radius: 12px; background: var(--surface-secondary);
  border: 1px solid var(--card-border); }
.ot-card b { font-size: 12px; color: var(--text-primary); }
.ot-sub { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 10px;
  color: var(--text-muted); }
.ot-meta { font-size: 10.5px; color: var(--text-secondary); }
.ot-ds { margin-top: 4px; font-size: 10.5px; color: var(--state-ok); text-decoration: none; }
.ot-ds:hover { text-decoration: underline; }
/* "No datasheet published" is a real answer about a commodity part, not a
   missing feature — stated in muted text, never left blank. */
.ot-ds.is-none { color: var(--text-muted); }

/* -- port budget ---------------------------------------------------------- */

.mc-ports { display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  padding: 9px 12px; margin-bottom: 16px; border-radius: 12px;
  background: var(--surface-secondary); border: 1px solid var(--border-light);
  font-size: 11.5px; color: var(--text-secondary); }
.mc-ports b { color: var(--text-primary); }
.mc-res { padding: 2px 8px; border-radius: 999px; background: var(--state-warn-bg);
  color: var(--state-warn); font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 10.5px; }

/* -- grid ----------------------------------------------------------------- */

/* align-items:stretch + a flex card keeps every tile in a row the same height,
   so a two-line module name does not make its neighbours look broken. */
/* 124px, not the 148px this grid used while it held twelve cards. The tile no
   longer has to be wide enough for a 173px-tall photograph — the thumbnail is
   capped at 96px — and 63 cards at four columns is sixteen rows of scrolling.
   Measured at an 810px column: 148px gives 4 columns / 16 rows, 124px gives 5
   columns / 13 rows. `min(100%, …)` so a narrow drawer column never produces a
   track wider than the grid itself, which is what forces horizontal overflow. */
.mc-grid { display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 124px), 1fr));
  gap: 10px; padding-bottom: 18px; align-items: stretch; }

/* Card + its add action stack as one grid cell, so the buttons line up across
   a row even when a module name wraps to two lines. */
.mc-cell { display: flex; flex-direction: column; gap: 6px; }
.mc-cell .mc-card { flex: 1; }

.mc-add { font: inherit; font-size: 11px; font-weight: 600; padding: 6px 9px;
  border-radius: 999px;
  cursor: pointer; background: var(--button-secondary-bg);
  color: var(--button-secondary-text); border: 1px solid var(--button-secondary-border);
  transition: border-color .15s ease, background .15s ease; }
.mc-add:hover:not(:disabled) { border-color: var(--border-dark);
  background: var(--button-secondary-bg-hover); }
.mc-add:disabled { opacity: .45; cursor: not-allowed; }

/* The same slot on a KY part or a dev board: a LABEL, not a button. These clip
   into no port, so an add action on one would be exactly the lie the port
   compatibility check exists to prevent — and a control that cannot act must
   not look actionable. No border, no hover, no pointer. */
/* nowrap + ellipsis so it occupies exactly one line, the same height as the
   `.mc-add` button beside it in the next column — a label that wraps to two
   lines makes its whole grid row taller than the buttons it sits among. */
.mc-noadd { font-size: 10px; line-height: 1.4; padding: 7px 2px;
  color: var(--text-muted); font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  text-align: center; cursor: default;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The action row: the add button (or the hand-wired label) and the buy link,
   side by side. It is a flex row rather than the two elements being siblings of
   `.mc-cell` so that a card with NO buy link renders exactly as it did before —
   one full-width action, no reserved gap for a link that does not exist.
   `min-width: 0` on the first child is what lets `.mc-noadd`'s ellipsis still
   work once it shares the row. */
.mc-actions { display: flex; gap: 6px; align-items: stretch; }
.mc-actions > :first-child { flex: 1; min-width: 0; }
/* An action row with nothing in it is not a row. On the standalone catalog no
   card has an add button, and since the 37 kit parts stopped rendering a
   redundant per-part buy link (see _buyLink) they have no action either — so
   without this every one of those cells still paid `.mc-cell`'s 6px gap for an
   element of height 0 with nothing to show. */
.mc-actions:empty { display: none; }

/* The buy link is a LINK and is styled as one thing that leaves the app: it
   sits outside the card button (an <a> inside a <button> is invalid HTML and
   the browser hoists it out), and it is deliberately quieter than `.mc-add` —
   adding a part to the board is the action this page is for, and going to a
   vendor is a detour. */
.mc-buy { font-size: 10.5px; padding: 6px 10px; border-radius: 999px;
  display: inline-flex; align-items: center; text-decoration: none;
  color: var(--text-secondary); background: transparent;
  border: 1px solid var(--card-border); white-space: nowrap;
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  transition: border-color .15s ease, color .15s ease; }
.mc-buy:hover { color: var(--text-primary); border-color: var(--border-medium); }

/* The atech-card recipe at grid size: hairline border, near-invisible resting
   shadow, border does the work on hover. 16px — the tile step of the radius
   scale — because these cells run down to 124px wide; Atech-UI itself uses the
   16px tier for its dense inner tiles and keeps 24px for full-width cards. */
.mc-card { padding: 0; overflow: hidden; cursor: pointer; text-align: left;
  border-radius: 16px; background: var(--card-bg);
  border: 1px solid var(--card-border); font: inherit;
  box-shadow: var(--glass-card-shadow);
  display: flex; flex-direction: column;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
.mc-card:hover { border-color: var(--card-border-hover);
  box-shadow: var(--glass-card-shadow-hover); transform: translateY(-1px); }
.mc-card.on { border-color: var(--border-dark); box-shadow: var(--glass-card-shadow-hover); }

/* The source PNGs are NOT square (dc_motor and microphone are strongly portrait),
   so the tile fixes its own box and the image is contained inside it. Without the
   explicit min-height:0 the grid row lets a tall image stretch the whole card.

   A CAPPED HEIGHT, not aspect-ratio:1. Measured at an 810px catalog column,
   `aspect-ratio: 1` gives a 173px thumbnail and a 248px card — fine for 12
   cards, but this grid now holds 63, and 63 × 248px is a taller wall than the
   2686px panel this redesign exists to replace. 96px keeps every part
   recognisable while the grid stays scannable. `aspect-ratio: auto` is stated
   explicitly because the height alone would not override the old ratio. */
.mc-thumb { position: relative; height: 96px; aspect-ratio: auto;
  display: grid; place-items: center;
  background: var(--surface-secondary); padding: 10px; overflow: hidden;
  flex: none; min-height: 0; }
.mc-thumb img { max-width: 100%; max-height: 100%; width: auto; height: auto;
  object-fit: contain; transition: transform .18s ease; }
.mc-card:hover .mc-thumb img { transform: scale(1.05); }
/* A rendered envelope, not a photograph (the 37 KY parts).

   These arrive as OPAQUE PNGs on a flat dark ground: p2p/ky_thumbs.py records
   that trimesh renders through pyglet/OpenGL into a framebuffer with no alpha,
   so `background=[0,0,0,0]` returns an RGBA PNG whose corner pixel measures
   (0,0,0,255) — opaque black wearing an alpha channel. Verified there, not
   assumed here, and its docstring names the CSS card background as the fix.

   That fix is this rule. Without it the tile is a hard black rectangle sitting
   on a white cell in light theme — measured: `.mc-thumb` is rgb(249,250,251)
   against the render's rgb(24,26,31), which reads as a broken or still-loading
   image rather than as a picture of a part. Giving the CELL the render's own
   ground makes the tile edge disappear and the part sit in a deliberate dark
   well, in both themes.

   The corner-radius inset keeps the well inside the card's own rounding. */
.mc-thumb:has(.mc-img-derived) { background: rgb(24, 26, 31); }
.mc-img-derived { opacity: .96; }
.mc-noimg { font-size: 10.5px; color: var(--text-muted); }

/* The generated placeholder. Measured 2026-08-20 after the KY tiles landed:
   the 14 off-the-shelf dev boards still reach this, and nothing else does.
   The 37 KY parts now carry a `thumb_url` rendered from their own envelope
   mesh, and 12 of 12 Atech modules carry a photograph. A dev board has neither
   a mesh nor a photo in this repo, so its tile stays a glyph — there is
   nothing measured to draw.
   Deliberately reads AS a placeholder — muted, borderless, a symbol rather than
   a picture — so it can never be mistaken for a photo of the part. The tag
   beside it is the part's own real identifier, so the tile still says something
   specific: "KY-018", "ESP32-S3". */
.mc-ph { display: flex; flex-direction: column; align-items: center; gap: 5px;
  color: var(--text-muted); }
.mc-ph svg { width: 28px; height: 28px; opacity: .55; }
.mc-phtag { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 9.5px;
  letter-spacing: .04em; opacity: .85; }
.mc-badge2 { position: absolute; top: 7px; right: 7px; padding: 2px 7px;
  border-radius: 999px; font-size: 9.5px; font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  background: var(--foreground); color: var(--background); }
/* The curated default set — hover carries the server's reason for the pick. */
.mc-badgerec { position: absolute; top: 7px; left: 7px; padding: 2px 7px;
  border-radius: 999px; font-size: 9.5px; font-weight: 700;
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  /* No literal fallback: tokens.css defines --accent unconditionally, and the
     old #0ff2b2 fallback was a stale copy of the pre-port aurora palette. */
  background: var(--accent); color: var(--background); }

.mc-body { padding: 8px 10px 9px; display: flex; flex-direction: column; gap: 1px;
  flex: 1; }
.mc-body .mc-meta { margin-top: auto; padding-top: 3px; }
.mc-cat { font-size: 9.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted); font-family: 'Geist Mono', ui-monospace, Menlo, monospace; }
/* The eyebrow leads with the TIER, and only the Atech one gets colour. In a
   merged grid the first thing worth knowing about a part is whether it clips
   into a numbered port or has to be soldered — that is what decides whether the
   action under it is a button at all. */
.mc-cat-atech { color: var(--state-ok); }
/* Clamped to two lines. `align-items: stretch` on the grid means every card in
   a row grows to the tallest one, so a single 4-line name — "Temperature sensor
   module (thermistor + comparator)", measured at 63px in a 146px column — added
   ~30px to four neighbours that did not need it. Across 63 cards that is
   hundreds of pixels of scrolling bought by one part's punctuation. The full
   name is never lost: it is the `title` on hover and the heading of the detail
   panel one click away. */
.mc-body b { font-size: 12.5px; color: var(--text-primary); line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden; }
/* One line, ellipsised, for the same stretch reason as the clamped title:
   "3.3V logic · 28 GPIO · runs Linux" wrapped to two lines and pulled four
   neighbouring cards up with it. The full string is the `title`, and every
   field in it is repeated in the detail panel. */
.mc-meta { font-size: 10.5px; color: var(--text-secondary);
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* The price line. Rendered ONLY when a price exists — there is no empty-state
   rule here on purpose, because `align-items: stretch` means one card growing a
   line grows every card in its row, and a reserved slot for a price that 51 of
   63 parts do not have would cost that height across the whole grid to say
   nothing. A part with no price simply has no line. */
.mc-pricerow { display: flex; align-items: baseline; gap: 6px;
  padding-top: 4px; min-width: 0; }
.mc-price { font-size: 11.5px; font-weight: 600; color: var(--text-primary);
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace; white-space: nowrap; }
/* The age of a scraped price, beside the number and visibly subordinate to it.
   It is small but it is NOT decoration: a price nobody has re-checked in six
   months is a different fact from one checked yesterday, and the number alone
   cannot say which. Ellipsised rather than wrapped for the same row-height
   reason as `.mc-meta`. */
.mc-priceage { font-size: 9.5px; color: var(--text-muted);
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* -- detail --------------------------------------------------------------- */

.mc-detail { display: none; }
.mc-detail.open { display: block; padding: 16px 20px 22px;
  border-top: 1px solid var(--border-light); background: var(--surface-secondary); }

/* Where `_revealDetail`'s scrollIntoView stops. The page headers on /modules
   and /bench are sticky, so `block: 'start'` alone parks the panel's heading
   underneath one of them; scroll-margin is the only offset that composes with
   scrollIntoView in whichever container actually scrolls (the document on the
   standalone pages, the drawer in the app). --navh is the header's MEASURED
   height, and the 0 fallback is what the drawer — which has no header — uses. */
.mc-detail { scroll-margin-top: calc(var(--navh, 0px) + 12px); }

/* The panel's heading takes focus when the panel opens (tabindex="-1"), and a
   focus ring is not inherited by a heading — so it is written here. Only
   :focus-visible: a mouse user who just clicked the card does not need a box
   drawn around the answer they asked for. */
.mc-detail h4:focus { outline: none; }
.mc-detail h4:focus-visible { outline: 2px solid var(--input-focus-ring);
  outline-offset: 3px; border-radius: 4px; }

/* Why this part has no "add to board" button, said once at the top of its own
   detail panel rather than only as a hover title down in the grid. */
.mc-offnote { padding: 10px 13px; margin-bottom: 14px; border-radius: 12px;
  font-size: 11.5px; line-height: 1.55; color: var(--text-secondary);
  background: var(--card-bg); border: 1px solid var(--border-light); }
.mc-offnote b { color: var(--text-primary); }

.mc-dhead { display: flex; justify-content: space-between; align-items: flex-start;
  gap: 12px; margin-bottom: 14px; }
.mc-dhead h4 { margin: 2px 0 2px; font-size: 16px; color: var(--text-primary); }
.mc-dhead .mc-dim { font-size: 11.5px; }
.mc-close { border: 1px solid var(--card-border); background: var(--card-bg);
  color: var(--text-secondary); width: 26px; height: 26px; border-radius: 999px;
  cursor: pointer; font-size: 15px; line-height: 1; flex: none; }
.mc-close:hover { border-color: var(--border-dark); color: var(--text-primary); }

.mc-dgrid { display: grid; grid-template-columns: minmax(200px, 1.1fr) 1fr;
  gap: 14px; margin-bottom: 14px; }
@media (max-width: 720px) { .mc-dgrid { grid-template-columns: 1fr; } }

/* The preview column: a switch row above, and the two representations below it.
   The column is the grid child now, not .mc-3d itself, so the chips scroll and
   collapse with the viewport they belong to rather than floating over it. */
.mc-pane { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
/* The representation switch, in the Atech-UI overlay-pill-group language. */
.mc-reprs { display: flex; gap: 2px; padding: 3px; align-self: start;
  background: var(--card-bg); border: 1px solid var(--card-border); border-radius: 999px; }
.mc-chip { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 10px; letter-spacing: .04em;
  padding: 4px 10px; border-radius: 999px; border: 1px solid transparent;
  background: transparent; color: var(--text-muted); cursor: pointer; line-height: 1.5; }
.mc-chip:hover { color: var(--text-primary); }
.mc-chip.on { background: var(--surface-secondary, var(--card-bg)); color: var(--acc-cyan);
  font-weight: 600; border-color: var(--card-border); }
.mc-chip:focus-visible { outline: 2px solid var(--acc-cyan); outline-offset: 1px; }

/* Both panes share the frame AND the height, so flipping between them does not
   resize the column — a switch that moves the layout under the cursor reads as
   a bug. Measured: the knob's photo is square at 851×851, so left to itself the
   photo pane grew to ~580px against the viewport's 210 and shoved every spec
   row down the page on each flip. The height is pinned rather than min-ed. */
.mc-3d, .mc-photo { position: relative; height: 210px; border-radius: 16px;
  background: var(--card-bg); border: 1px solid var(--card-border); overflow: hidden; }
.mc-photo { display: flex; align-items: center; justify-content: center; padding: 10px; }
/* CAD studio stills are opaque on a near-black ground — same trap as
   `.mc-img-derived` on the grid. */
.mc-photo.is-cad { background: rgb(24, 26, 31); padding: 0; }
/* `contain`, not `cover`: this is a product photo being inspected, and cropping
   the edges off a part someone is trying to identify defeats the point. */
.mc-photo img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.mc-photo.is-cad img { width: 100%; height: 100%; object-fit: cover; }
.mc-3dnote { position: absolute; left: 0; right: 0; bottom: 0; padding: 5px 9px;
  font-size: 10px; font-family: 'Geist Mono', ui-monospace, Menlo, monospace; color: var(--text-muted);
  background: linear-gradient(transparent, var(--card-bg) 55%); text-align: center; }
.mc-no3d { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 5px; padding: 18px;
  text-align: center; }
.mc-no3d b { font-size: 12.5px; color: var(--text-primary); }
.mc-no3d span { font-size: 11px; line-height: 1.5; color: var(--text-secondary);
  max-width: 34ch; }

.mc-specs { display: flex; flex-direction: column; gap: 0; align-self: start; }
.mc-row { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--border-light);
  font-size: 12px; }
.mc-row:last-child { border-bottom: 0; }
.mc-row span { color: var(--text-muted); }
.mc-row b { color: var(--text-primary); font-weight: 600; text-align: right;
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 11.5px; }
/* One row on this table holds a SENTENCE, not a measurement: `port_note` reads
   "Takes 2 adjacent ports; needs a port carrying digital". The default above is
   built for values like "60.0 × 7.0 × 120.0 mm" — right-aligned, monospace,
   nowrap-ish — and applied to prose it produced a ragged right-to-left block of
   11.5px code type in a 1fr column. Prose gets the body font, the reading
   direction, and permission to wrap. */
.mc-row.is-prose { align-items: start; }
/* `flex: 1` is what makes `text-align: left` mean anything here. The row is a
   space-between flex line, so the value box shrink-wraps its own text and sits
   flush right no matter what its text-align says — measured after the first
   attempt at this fix, which set text-align:left and changed nothing on screen.
   Growing the box is the actual left-alignment. */
.mc-row.is-prose b { flex: 1; min-width: 0; text-align: left; white-space: normal;
  font-weight: 500; font-family: inherit; font-size: 12px; line-height: 1.5; }

.mc-desc { margin: 0 0 12px; font-size: 12.5px; line-height: 1.6;
  color: var(--text-secondary); max-width: 78ch; }

/* Section heads inside the detail panel: the mono-label recipe. */
.mc-sec { font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted); font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  margin: 14px 0 7px; }

.mc-caps { list-style: none; margin: 0 0 4px; padding: 0;
  display: flex; flex-direction: column; gap: 5px; }
.mc-caps li { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-size: 11.5px; padding: 5px 9px; border-radius: 8px;
  background: var(--card-bg); border: 1px solid var(--border-light); }
.mc-caps code { font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 11px;
  color: var(--text-primary); font-weight: 600; }
.mc-caps span { color: var(--text-secondary); flex: 1; min-width: 12ch; }
.mc-caps em { font-style: normal; font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  font-size: 10px; color: var(--text-muted); padding: 1px 6px; border-radius: 999px;
  background: var(--surface-secondary); }

.mc-hints { margin: 0; padding: 11px 13px; border-radius: 12px;
  background: var(--card-bg); border: 1px solid var(--border-light);
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace; font-size: 11px; line-height: 1.65;
  color: var(--text-secondary); white-space: pre-wrap; overflow-x: auto;
  max-height: 300px; overflow-y: auto; }

/* The vendor photograph in the off-tier detail panel.
   ---------------------------------------------------------------------------
   These are cached third-party bytes (see p2p/part_images.py), shown on the
   condition that the source is credited — so the caption is part of the
   component, not decoration that a later layout change may drop.

   `contain` rather than `cover`: these are cut-out product shots of boards with
   very different aspect ratios (a 15mm photoresistor next to a 32mm relay), and
   `cover` would crop the pin header off exactly the parts whose header is the
   thing worth looking at. Letterboxing an honest picture beats cropping it.

   Tokens are the real names from tokens.css, checked rather than guessed: an
   unknown custom property resolves to the empty string and fails silently, with
   nothing in any console to say so. */
.ot-photo {
  margin: 12px 0 0;
  padding: 10px;
  border: 1px solid var(--border-light);
  border-radius: 12px;
  background: var(--surface-secondary);
}
.ot-photo img {
  display: block;
  width: 100%;
  max-height: 220px;
  object-fit: contain;
}
.ot-photo figcaption {
  margin-top: 8px;
  font-size: 10.5px;
  color: var(--text-muted);
}
.ot-photo figcaption a { color: var(--text-secondary); }

/* --- pinout, from Atech's own hardware docs ------------------------------ */
/* Added 2026-08-29 with the datasheets parser rewrite. /api/catalog had been
   serving `schematic.pins` to nothing at all: the parser broke against a doc
   rewrite (../atech_docs commit 886064e, 2026-08-12 — seventeen days, measured,
   not the "months" the first write-up guessed) and returned an empty list for
   every module, and no screen would have looked different if it had never been
   fixed. */

.mc-pins { display: flex; flex-wrap: wrap; gap: 10px; }
/* One port should not stretch a six-row table across the full 1140px panel:
   the same data then reads at two very different densities depending on how
   many ports a module happens to take. Capped at the width two tables share. */
.mc-pins.is-single .mc-pintab { max-width: 565px; }
.mc-pintab { border-collapse: collapse; font-size: 11px; flex: 1 1 260px;
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  background: var(--card-bg); border: 1px solid var(--border-light);
  border-radius: 10px; overflow: hidden; }
.mc-pintab caption { caption-side: top; text-align: left; padding: 6px 9px 4px;
  font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-muted); }
.mc-pintab th { text-align: left; font-weight: 600; font-size: 10px;
  letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted);
  padding: 5px 9px; border-bottom: 1px solid var(--border-light); }
.mc-pintab td { padding: 4px 9px; color: var(--text-secondary);
  border-bottom: 1px solid var(--border-light); }
.mc-pintab tr:last-child td { border-bottom: 0; }
.mc-pintab td:first-child { color: var(--text-primary); font-weight: 600;
  font-variant-numeric: tabular-nums; width: 1%; }
.mc-pintab td:last-child { color: var(--text-primary); }
/* A not-connected pin is REAL information (the module ignores that line), so it
   is shown and dimmed rather than hidden — dropping the row would renumber the
   table against the silkscreen. */
.mc-pintab tr.is-nc td { color: var(--text-muted); font-style: italic; }
.mc-pintab tr.is-nc td:first-child,
.mc-pintab tr.is-nc td:last-child { color: var(--text-muted); font-weight: 400; }

.mc-revs { font-style: normal; font-weight: 500; text-transform: none;
  letter-spacing: 0; color: var(--text-muted); }

/* Where the numbers came from. Small, always present when there is a table:
   a pinout with no source is the thing this whole file exists to prevent.

   `.mc-prov`, NOT `.mc-src` — that name is already taken at the top of this
   file by the sourcing block, at equal specificity, and the shorthand `margin`
   here would have silently zeroed its `margin-bottom`. Nothing renders that
   markup today (`.mc-srclabel` and `.mc-opt` match zero elements), which is
   exactly why the collision would have gone unnoticed until it came back. */
.mc-prov { margin: 6px 0 0; font-size: 10.5px; color: var(--text-muted); }
.mc-prov code { font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  font-size: 10px; word-break: break-all; }

/* The stated reason there is no table. Never styled as an error: "the docs are
   not on this server" is a deployment fact, not a defect in the hardware. */
.mc-nopin { color: var(--text-muted); }

.mc-chiprow { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
  font-size: 11.5px; padding: 6px 10px; border-radius: 8px;
  background: var(--card-bg); border: 1px solid var(--border-light); }
.mc-chiprow code { font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  font-size: 11px; font-weight: 600; color: var(--text-primary); }
.mc-chiprow span { color: var(--text-secondary); flex: 1; min-width: 10ch; }
/* --accent resolves through --aurora-cyan; body-sized link text goes through
   --accent-ink so it clears WCAG AA on this row's white card at 11.5px (dark
   only just clears 4.50:1). The token stays correct at the larger sizes it is
   used at elsewhere. No `var(...)` fallback: tokens.css defines these
   unconditionally, and a fallback that can never fire reads as a fallback
   that might. */
.mc-chiprow a { color: var(--accent-ink); text-underline-offset: 2px; }
.mc-chiprow .is-none { color: var(--text-muted); flex: none; }
/* The part number as printed on the hardware, where it differs from the chip
   the datasheet covers. Dimmed: it is the second answer, not a competing one. */
.mc-chiprow .mc-printed { color: var(--text-muted); font-weight: 500; }

/* Visually hidden, still announced. Used for the single-port table's caption:
   a <table> with no accessible name tells a screen reader nothing about what
   it is, and the label above it is a <div>, not a heading. */
.mc-pintab caption.sr-only { position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
  white-space: nowrap; border: 0; }

/* The doc's own `status:` where it is not plain `available` — firmware-only,
   no-driver, driver-only, passive, coming-soon. Served for all 12 modules and
   rendered for none until 2026-08-29: it only appeared in the no-pins branch,
   which no catalog module reaches. A chip, not an alarm: "firmware-only" is a
   fact about driver coverage, not a fault. */
.mc-maturity { display: inline-block; margin: 12px 0 -4px;
  font-family: 'Geist Mono', ui-monospace, Menlo, monospace;
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-secondary); background: var(--surface-secondary);
  border: 1px solid var(--border-light); border-radius: 999px;
  padding: 2px 9px; }
