/* Shared stylesheet for the generated pages (/store/ and /plugins/<slug>/).
   Copied to assets/site.css by scripts/build.mjs.

   index.html, support/index.html, and resume/index.html keep their own inline
   <style> blocks. The tokens and the topbar/footer rules below are transcribed
   from those pages so the new surfaces match exactly. If a token changes there,
   change it here too. */

:root {
  --bg: #101317;
  --bg-raised: #161b21;
  --panel: #1a2028;
  --key-face: #232a33;
  --line: #2a323d;
  --gold: #c8962e;
  --gold-bright: #e8b54a;
  --paper: #ede7da;
  --slate: #97a1ac;
  --slate-dim: #6b7580;
  --led-live: #3fd47e;
  --led-review: #e8b54a;
  --led-built: #58a6ff;
  --led-dev: #8b949e;
  --font-display: "Big Shoulders", "Arial Narrow", sans-serif;
  --font-body: "IBM Plex Sans", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;
  --accent: var(--gold);
  --hover: #2ee6c9;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

body {
  background: var(--bg);
  color: var(--slate);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; }

/* 1120px matches index.html's .wrap exactly. It used to be 880 here with a
   1180 .wrap-wide for galleries, which meant three different column widths on
   one site: the topbar shifted 30px when you navigated from the home page to a
   plugin page, and inside a plugin page the left edge jogged 150px between the
   header and the gallery. One width now, everywhere, and prose is held to a
   readable measure by max-width on the text itself (see .lede / .page-head p),
   the same way support/index.html already did it. */
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 24px; }

a { color: var(--gold-bright); text-decoration: none; }
a:hover { color: var(--hover); text-decoration: underline; }
a:focus-visible { outline: 2px solid var(--hover); outline-offset: 3px; border-radius: 2px; }

/* ---------- top bar ---------- */
.topbar {
  border-bottom: 1px solid var(--line);
  background: rgba(16, 19, 23, 0.92);
  position: sticky; top: 0; z-index: 20;
  backdrop-filter: blur(8px);
}
.topbar .wrap {
  display: flex; align-items: center; justify-content: space-between;
  padding-top: 14px; padding-bottom: 14px;
}
.brand { display: flex; align-items: center; gap: 12px; color: var(--paper); }
.brand:hover { text-decoration: none; color: var(--paper); }
.brand img { height: 40px; width: auto; display: block; }
.brand-name {
  font-family: var(--font-display);
  font-weight: 700; font-size: 1.15rem; letter-spacing: 0.06em;
  text-transform: uppercase; line-height: 1.1;
}
.brand-name small {
  display: block; font-family: var(--font-mono); font-weight: 400;
  font-size: 0.62rem; letter-spacing: 0.14em; color: var(--gold);
  text-transform: uppercase;
}
.topnav {
  display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12px 28px;
  font-family: var(--font-mono); font-size: 0.78rem;
  letter-spacing: 0.08em; text-transform: uppercase;
}
/* The ROW wraps, never the labels. A nowrap flex row answers a bar narrower
   than its tabs by shrinking the items, which broke the words themselves:
   "WHAT WE / BUILD", "WHO WE / ARE", all of them at 768. Wrapping the row
   instead keeps every label whole and spends one extra line, which is the trade
   the 1040 tightening was already making in miniature. */
.topnav a { color: var(--slate); white-space: nowrap; }
.topnav a:hover { color: var(--hover); text-decoration: none; }
.topnav a[aria-current="page"] { color: var(--gold); }
/* Six tabs since Buy direct retired into Store. The tightened gap and type
   between 640 and 1040 hold the row on one line down to about 930, which is
   where six tabs held before the retired tab ever joined the bar. Transcribed into
   index.html and support/index.html in the same commit, per the three-surfaces
   rule. */
@media (max-width: 1040px) { .topnav { gap: 15px; font-size: 0.71rem; } }

/* ---------- the phone menu ----------
   Below 640 the tab row does not fit, and it used to be display:none with
   nothing put in its place, which left a phone with no navigation on any page
   of this site. The row itself becomes the sheet now: same nav element, same
   links, same aria-current marking, so there is never a second list to keep in
   sync with the first.

   Absolutely positioned against .topbar, which is sticky and therefore a
   positioned ancestor, so the sheet hangs under the bar edge to edge and the
   page below it does not move when it opens. Closed it is display:none, so it
   costs no layout at all. The ground is opaque on purpose: the bar itself is
   92% alpha over a blur, and a translucent sheet on top of a scrolling list of
   products is unreadable.

   With JavaScript off the toggle is exactly what the markup says, a link to
   #nav, and :target opens the sheet. assets/nav.js upgrades it: it cancels the
   jump so no hash reaches the URL, moves focus into the sheet, and closes on
   Escape, on a tap outside, and on picking a link. */
.navtoggle { display: none; }
@media (max-width: 640px) {
  .topnav {
    display: none;
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; flex-wrap: nowrap; justify-content: flex-start;
    gap: 0; padding: 4px 0 8px;
    background: var(--bg); border-bottom: 1px solid var(--line);
    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.55);
  }
  .topnav:target, .topnav.is-open { display: flex; }
  /* Full-width rows, so the tap target is the line and not the word. */
  .topnav a { padding: 11px 24px; }
  .navtoggle {
    display: inline-flex; align-items: center; gap: 9px;
    font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--slate); padding: 6px 0;
  }
  .navtoggle:hover { color: var(--hover); text-decoration: none; }
  .navtoggle svg { display: block; }
  /* The toggle goes gold while the sheet is down, which is the same thing gold
     means on a tab: this is where you are. Works on :target too, so the no-JS
     path says it as well. */
  .topnav:target ~ .navtoggle, .topnav.is-open ~ .navtoggle { color: var(--gold); }
}

/* ---------- page head ---------- */
.page-head { padding: 64px 0 44px; border-bottom: 1px solid var(--line); }
.eyebrow {
  font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--accent);
  display: flex; align-items: center; gap: 10px; margin-bottom: 18px;
}
.eyebrow::before { content: ""; width: 34px; height: 1px; background: var(--accent); }
h1 {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2.2rem, 5.5vw, 3.6rem); line-height: 1;
  letter-spacing: 0.01em; text-transform: uppercase; color: var(--paper);
  margin-bottom: 18px;
}
h1 .gold { color: var(--gold); }
.lede { max-width: 62ch; font-size: 1.04rem; }
.lede strong { color: var(--paper); font-weight: 600; }
/* Phones. The head is the same component on every generated page, and at 390 it
   was spending 538px, most of a screen, before any page's actual content
   started. Measured on the store, which has the longest one: 64px of padding
   above an eyebrow, a 35px display h1 that wraps to two lines, a lede at
   desktop size, and 44px under it. Trimmed here rather than per page, so every
   head shrinks by the same amounts and none of them drift apart. */
@media (max-width: 640px) {
  .page-head { padding: 30px 0 26px; }
  .eyebrow { margin-bottom: 13px; }
  h1 { font-size: 1.95rem; margin-bottom: 13px; }
  .lede { font-size: 0.97rem; }
}

/* ---------- buttons ---------- */
.cta-row { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 26px; }
.btn {
  display: inline-block; padding: 13px 26px;
  font-family: var(--font-mono); font-size: 0.82rem; letter-spacing: 0.1em;
  text-transform: uppercase; border-radius: 4px; border: 1px solid transparent;
}
.btn-gold { background: var(--gold); color: #14110a; font-weight: 500; }
.btn-gold:hover { background: var(--gold-bright); color: #14110a; text-decoration: none; }
.btn-ghost { border-color: var(--line); color: var(--slate); }
.btn-ghost:hover { border-color: var(--hover); color: var(--hover); text-decoration: none; }
.btn-lg { padding: 16px 34px; font-size: 0.88rem; }

/* ---------- status pips (transcribed from index.html) ---------- */
.status {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
  text-transform: uppercase;
}
.status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--led); box-shadow: 0 0 5px var(--led); }
.status-live { --led: var(--led-live); color: var(--led-live); }
.status-review { --led: var(--led-review); color: var(--led-review); }
.status-built { --led: var(--led-built); color: var(--led-built); }
.status-dev { --led: var(--led-dev); color: var(--led-dev); }

/* ---------- sections ---------- */
section { padding: 56px 0; border-bottom: 1px solid var(--line); scroll-margin-top: 80px; }
h2 {
  font-family: var(--font-display); font-weight: 800;
  font-size: clamp(1.6rem, 3.5vw, 2.3rem); text-transform: uppercase;
  letter-spacing: 0.02em; color: var(--paper); line-height: 1;
  margin-bottom: 24px;
}
h3 {
  font-family: var(--font-display); font-weight: 700; font-size: 1.3rem;
  text-transform: uppercase; letter-spacing: 0.03em; color: var(--paper);
  margin-bottom: 12px;
}

/* ---------- plugin page: specs + buy ---------- */
.plugin-head h1 { color: var(--paper); }
.specs {
  display: flex; flex-wrap: wrap; gap: 10px; list-style: none; margin: 26px 0 0;
}
.specs li {
  display: flex; flex-direction: column; gap: 1px;
  padding: 10px 18px; border-radius: 8px;
  background: linear-gradient(180deg, #2a323c, var(--key-face) 60%);
  border: 1px solid #333c47;
  box-shadow: inset 0 1px 3px rgba(255, 255, 255, 0.05), inset 0 -2px 4px rgba(0, 0, 0, 0.4);
  min-width: 92px;
}
.spec-n {
  font-family: var(--font-display); font-weight: 700; font-size: 1.5rem;
  line-height: 1; color: var(--accent);
}
.spec-l {
  font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--slate-dim);
}

.buy { margin-top: 30px; }
.buy-row { display: flex; gap: 14px; flex-wrap: wrap; }
.buy-note {
  margin-top: 10px; font-family: var(--font-mono); font-size: 0.72rem;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--slate-dim);
}
.buy-note strong { color: var(--led-live); font-weight: 500; }
.buy-pending {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 16px 20px; border-radius: 8px; max-width: 62ch;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  font-size: 0.93rem;
}

/* ---------- plugin page: gallery ---------- */
.shot { margin: 0; }
.shot a { display: block; }
.shot a:hover { text-decoration: none; }
.shot img {
  display: block; width: 100%; height: auto; aspect-ratio: 2 / 1;
  object-fit: cover; border-radius: 8px;
  border: 1px solid var(--line); background: var(--panel);
}
/* Claude Complete Controller's slides are 16:9, not 2:1. Cover-cropping them
   ate the title, so letterbox instead. Same call as the support hub. */
.shot.ratio-16-9 img { aspect-ratio: 16 / 9; object-fit: contain; background: #0b0d10; }
/* One slide per row. The old two-up grid rendered these 1920px information
   graphics at 555px, which put their baked-in body copy near 5px tall: real
   information reduced to wallpaper. Full column width is ~57% scale and
   legible, which is the whole point of showing the slide at all. */
.shot-list { display: grid; grid-template-columns: 1fr; gap: 26px; }

/* ---------- notes lists ---------- */
/* Capped: the wrap is 1120 now, and uncapped list copy ran 1072px lines. */
.notes { margin: 0 0 0 20px; max-width: 68ch; }
.notes li + li { margin-top: 10px; }
.notes strong { color: var(--paper); font-weight: 600; }
.notes code {
  font-family: var(--font-mono); font-size: 0.85em;
  background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
  padding: 1px 6px; color: var(--gold-bright);
}
.req-head { margin-top: 32px; }
.req { max-width: 66ch; }
.detail-cta { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 34px; }

/* ---------- plugin page: support ---------- */
.support-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start; }
@media (max-width: 820px) { .support-cols { grid-template-columns: 1fr; gap: 32px; } }
.support-more { margin-top: 14px; font-size: 0.92rem; }

/* ---------- store: cards ---------- */
.card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
@media (max-width: 1000px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 660px) { .card-grid { grid-template-columns: 1fr; } }

.card {
  display: flex; flex-direction: column;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-top: 3px solid var(--accent);
  border-radius: 8px; overflow: hidden; color: var(--slate);
  transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.card:hover {
  text-decoration: none; transform: translateY(-3px);
  color: var(--slate);
  border-color: var(--accent);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5), 0 0 0 1px var(--accent);
}
.card-shot {
  display: block; width: 100%; height: auto; aspect-ratio: 2 / 1;
  object-fit: cover; border-bottom: 1px solid var(--line); background: var(--panel);
}
.card-shot.ratio-16-9 { aspect-ratio: 16 / 9; object-fit: contain; background: #0b0d10; }
.card-body { padding: 18px 22px 20px; display: flex; flex-direction: column; flex: 1; }
.card h3 { font-size: 1.22rem; margin-bottom: 3px; }
.card:hover h3 { color: var(--hover); }
.card-meta {
  font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 12px;
}
.card-note { font-size: 0.92rem; flex: 1; }
.card-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  flex-wrap: wrap; margin-top: 16px; padding-top: 14px;
  border-top: 1px solid var(--line);
}
.card-go {
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--slate-dim);
}
.card:hover .card-go { color: var(--hover); }

/* ---------- footer ---------- */
footer { padding: 40px 0 48px; }
footer .wrap {
  display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--slate-dim);
}
/* 34px, matching index.html. It was 64px here, so the footer logo rendered
   nearly twice as tall on the generated pages as on the hand-written ones. */
footer img { height: 34px; opacity: 0.7; }

/* ------------------------------------------------ command explorer (/commands/)
   Trailhead surface. Gold is the rest state, teal is the touch response, and the
   per-plugin accent rides on the row edge and the plugin chip so a result carries
   its product identity without a second color system. */

.cx-head { margin: 0 0 6px; }
.cx-lede { color: var(--slate); max-width: 62ch; margin: 0 0 22px; }

.cx-tools { position: sticky; top: 0; z-index: 5; padding: 14px 0 12px;
  background: linear-gradient(var(--bg) 78%, transparent); }

.cx-searchwrap { position: relative; display: flex; align-items: center; }
.cx-searchwrap input {
  width: 100%; box-sizing: border-box;
  font-family: var(--font-body); font-size: 1.05rem; color: var(--paper);
  background: var(--key-face); border: 1px solid var(--line); border-radius: 10px;
  padding: 13px 96px 13px 15px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
}
.cx-searchwrap input::placeholder { color: var(--slate-dim); }
.cx-searchwrap input:focus { outline: none; border-color: var(--hover);
  box-shadow: 0 0 0 3px rgba(46,230,201,.13); }
.cx-slash { position: absolute; right: 13px; color: var(--slate-dim);
  font-family: var(--font-mono); font-size: .74rem; pointer-events: none; }
.cx-clear { position: absolute; right: 46px; background: none; border: 0;
  color: var(--slate); cursor: pointer; font-size: 1.2rem; line-height: 1; padding: 4px 6px; }
.cx-clear:hover { color: var(--hover); }
/* The clear button and the "press /" hint occupy the same corner. Once there is
   something to clear, the hint has done its job, so it yields rather than
   stacking under the button. */
#cx-clear:not([hidden]) ~ .cx-slash { display: none; }
/* "press /" is a keyboard affordance, and it was advertising a key that half
   the visitors to these two pages do not have. Gone on touch and on anything
   narrow, and the 96px of right padding it was reserving goes back to the
   placeholder, which was arriving clipped mid-word at 390. The clear button
   takes the corner the hint vacated. */
@media (hover: none), (max-width: 640px) {
  .cx-slash { display: none; }
  .cx-searchwrap input { padding-right: 44px; }
  .cx-clear { right: 13px; }
}

.cx-filters { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
.cx-filters + .cx-filters { margin-top: 7px; }
.cx-chip {
  font-family: var(--font-body); font-size: .78rem; color: var(--slate);
  background: var(--panel); border: 1px solid var(--line); border-radius: 999px;
  padding: 5px 11px; cursor: pointer; transition: color .12s, border-color .12s;
}
.cx-chip:hover { color: var(--hover); border-color: var(--hover); }
.cx-chip[aria-pressed="true"] {
  color: var(--bg); background: var(--chip-accent, var(--gold));
  border-color: var(--chip-accent, var(--gold)); font-weight: 600;
}

.cx-count { font-family: var(--font-mono); font-size: .78rem; color: var(--slate-dim);
  margin: 14px 0 8px; }
.cx-status { color: var(--slate); font-size: .9rem; }
.cx-status-bad { color: var(--led-review); }

.cx-list { list-style: none; margin: 0; padding: 0; }
.cx-row {
  display: grid; grid-template-columns: minmax(0,2.1fr) minmax(0,1.2fr) minmax(0,1.1fr) auto;
  gap: 14px; align-items: center;
  padding: 9px 14px; border: 1px solid var(--line); border-left: 3px solid var(--row-accent, var(--line));
  border-radius: 7px; background: var(--bg-raised); margin-bottom: 5px;
}
.cx-row:hover { border-color: var(--line); background: var(--panel); }
.cx-name { color: var(--paper); font-weight: 500; }
.cx-cat, .cx-plugin { font-size: .82rem; color: var(--slate-dim); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cx-plugin { color: var(--slate); text-decoration: none; }
.cx-plugin:hover { color: var(--hover); }
.cx-keys { display: flex; align-items: center; gap: 3px; justify-content: flex-end; }
.cx-keys kbd {
  font-family: var(--font-mono); font-size: .72rem; color: var(--paper);
  background: var(--key-face); border: 1px solid var(--line);
  border-bottom-width: 2px; border-radius: 4px; padding: 3px 6px; white-space: nowrap;
}
.cx-plus { color: var(--slate-dim); font-size: .7rem; }
.cx-tag { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .04em;
  border-radius: 4px; padding: 3px 7px; white-space: nowrap; }
.cx-tag-uia { color: var(--led-built); border: 1px solid rgba(88,166,255,.3); }
.cx-tag-none { color: var(--slate-dim); border: 1px solid var(--line); }
.cx-empty { color: var(--slate); padding: 26px 4px; }
.cx-empty strong { color: var(--paper); }

.cx-more { display: block; margin: 16px auto 0; font-family: var(--font-body);
  font-size: .85rem; color: var(--slate); background: var(--panel);
  border: 1px solid var(--line); border-radius: 8px; padding: 9px 20px; cursor: pointer; }
.cx-more:hover { color: var(--hover); border-color: var(--hover); }

@media (max-width: 760px) {
  .cx-row { grid-template-columns: 1fr auto; row-gap: 5px; }
  .cx-cat { grid-column: 1; }
  .cx-plugin { grid-column: 1; }
  .cx-keys { grid-row: 1 / span 3; grid-column: 2; align-self: start; }
}

/* ------------------------------------------------------- showroom (/store/)
   Trailhead surface. Two panes that are SIBLINGS IN A GRID, never layers: a
   scrolling rail listing every plugin on the left, one detail pane on the
   right. Neither can cover the other, which is the whole reason this layout
   replaced the key wall it grew out of.

   The wall's readout was sticky to the bottom of the VIEWPORT, and sticky
   positioning reserves no space in the flow, so nothing below it knew it was
   there and it rendered on top of live keys at most scroll positions. Here the
   detail pane sticks inside its own grid column: a sticky box cannot escape its
   containing block, so the worst it can do is stop moving. The tools above it
   do not stick at all, so there is nothing left for it to collide with even in
   principle.

   Filtering REMOVES rows. It does not dim them. A rail of dimmed unselectable
   rows is worse than a short rail, and the count line says how short. */

.sr-banner {
  margin-top: 22px; max-width: 68ch;
  padding: 14px 18px; border-radius: 8px;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-left: 3px solid var(--gold); font-size: 0.93rem;
}
.sr-banner strong { color: var(--paper); font-weight: 600; }

/* NOT sticky, deliberately. On the key wall these sat at top:68px, which put a
   second pinned surface on a page that already had a pinned topbar and a pinned
   readout, and every pinned surface is one more thing that can land on top of
   the content. The "/" shortcut brings the search back from any scroll
   position, which is what pinning was buying. */
.sr-tools { padding: 2px 0 16px; scroll-margin-top: 84px; }
/* Same yield as the command explorer: once there is something to clear, the
   "press /" hint has done its job and steps out from under the button. */
#sr-clear:not([hidden]) ~ .cx-slash { display: none; }
/* Chrome draws its own clear affordance inside type="search", which stacked a
   second X beside ours. Ours stays, because Firefox draws none. */
.sr-tools input[type="search"]::-webkit-search-cancel-button { display: none; }
/* --slate-dim is #6b7580, and on this page's two grounds it measures 3.69:1 on
   #161b21 and 3.96:1 on #101317. Both fail. The four places it landed on /store/
   are the field line under every product name, the result count, the note under
   the buy button, and the gallery heading, which is to say most of the page's
   supporting text. They read --slate instead: 6.6:1 on the panel, 7.1:1 on the
   page. The TOKEN is untouched on purpose. site.css is shared by every generated
   page, and --slate-dim is doing legitimate work elsewhere on grounds these
   numbers do not describe. */
.sr-count { font-family: var(--font-mono); font-size: .76rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--slate); margin: 12px 0 0; }
.sr-empty { color: var(--slate); padding: 26px 4px; max-width: 62ch; }
.sr-empty strong { color: var(--paper); }

/* The two panes. align-items:start is what lets the detail pane stick at all: a
   stretched grid item is already as tall as its row, so it would have nowhere
   to travel. */
.sr-browse {
  display: grid; grid-template-columns: 360px minmax(0, 1fr);
  gap: 26px; align-items: start;
}
/* Same width the topnav tightens at. Below it the rail gives up 60px rather
   than squeezing the pane that carries the price and the buttons. */
@media (max-width: 1040px) {
  .sr-browse { grid-template-columns: 300px minmax(0, 1fr); gap: 20px; }
}

/* ---------------------------------------------------------------- the rail */

.sr-railwrap { min-width: 0; }
.sr-rail { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px; }
/* The topbar is sticky at 69px and reserves nothing, so anything scrolled to the
   top of the viewport arrives underneath it. Walking the rail with the arrow
   keys focuses a row link, and focus() scrolls, so the row you just moved to was
   the one row you could not read. On both the row and its link: focus() scrolls
   the LINK, and a /store/#slug deep link scrolls the ROW. */
.sr-row, .sr-rowlink { scroll-margin-top: 84px; }
/* Filtered out means gone. The rule is written out because an author `display`
   on .sr-row would beat the [hidden] rule in the UA sheet; nothing sets one,
   and this is what makes that deliberate rather than lucky. */
.sr-rail .sr-row[hidden] { display: none; }

.sr-rowlink {
  display: grid; grid-template-columns: 30px minmax(0, 1fr) auto;
  gap: 12px; align-items: center;
  padding: 8px 12px 8px 10px;
  border: 1px solid var(--line); border-left: 3px solid transparent;
  border-radius: 7px; background: var(--bg-raised); color: var(--slate);
  transition: border-color .12s, background .12s;
}
/* Composite anchor: without this pin the generic a:hover teal runs into the
   row's name and field lines and the whole row changes color. Same defect
   design.md documents on .card and .brand, caught twice before. */
.sr-rowlink:hover {
  text-decoration: none; color: var(--slate);
  background: var(--panel); border-color: var(--line); border-left-color: var(--hover);
}
.sr-rowlink:focus-visible { outline: 2px solid var(--hover); outline-offset: 2px; }

/* The key art, reduced to the size a list row can carry. The full face belongs
   in the detail pane; here it is an identifier, not a product shot. */
.sr-chip {
  display: block; width: 30px; height: 30px; border-radius: 6px; overflow: hidden;
  background: var(--key-face);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 1px 3px rgba(0, 0, 0, .4);
}
.sr-chip img { display: block; width: 100%; height: 100%; object-fit: cover; }

.sr-rowtext { min-width: 0; }
/* The name wraps to a second line rather than ellipsising. On a single line in
   the 300px rail below 1040, four products lost the end of their own name:
   "Obsidian macOS Master Commander" ran 243px into 186px, and the word the
   ellipsis ate was the one that says which product it is. Two lines is the
   clamp, so a row can grow by one line and no further, and the ellipsis stays
   as the honest last resort. Above 1040 nothing wraps, because nothing has to:
   the widest name measures 243px in a 246px column. */
.sr-rowname {
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; line-clamp: 2;
  color: var(--paper); font-size: .92rem; line-height: 1.35;
  overflow: hidden;
}
.sr-rowfield {
  display: block; font-family: var(--font-mono); font-size: .6rem;
  letter-spacing: .09em; text-transform: uppercase; color: var(--slate);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sr-rowtag { font-family: var(--font-mono); font-size: .72rem; color: var(--slate);
  white-space: nowrap; }

/* --------------------------------------------------- the row hover preview

   Hovering a row opens that plugin's cover art inside the row. It is a PREVIEW
   and nothing else: it never selects, so the pane on the right does not change
   and neither does the selection. Click and Enter still own that.

   It grows DOWNWARD from a fixed top edge, so the pointer stays inside the row
   it opened; the rows below it are the only thing that moves, and the detail
   pane cannot move at all, because the rail is the taller of the two grid items
   and the pane's own column is what it sticks inside.

   Three gates, each load-bearing:

     hover / pointer: fine   a touch device has no hover, and :hover there
                             latches after a tap instead of following a pointer.
     min-width: 901px        below the .sr-browse collapse the detail already
                             opens inside the row, and a second thing growing in
                             the same place is noise. This 901, the 900 on
                             .sr-browse, the 900 on the chip rails, and the query
                             in showroom.js all have to agree.
     [data-peek]             set by showroom.js, which is also what moves the art
                             path into src. With no JavaScript the attribute
                             never arrives, so a row cannot open onto an empty
                             frame.

   No prefers-reduced-motion rule is needed: the global reset at the top of this
   file already kills every transition with !important. */
.sr-peek { display: none; }

@media (hover: hover) and (pointer: fine) and (min-width: 901px) {
  /* A 0fr-to-1fr grid row, not a max-height. max-height had to be a guess big
     enough for the tallest case, and 230px against art that measures 186px in a
     360px rail and 156px in a 300px one meant the reveal spent its first frames
     travelling through empty space and the close began with nothing moving.
     The grid row is the art's own height at every width, so the transition
     starts and ends on the picture. */
  .sr-rail[data-peek] .sr-peek {
    display: grid; grid-template-rows: 0fr; opacity: 0; cursor: pointer;
    transition: grid-template-rows .16s ease, opacity .12s ease;
  }
  .sr-rail[data-peek] .sr-peekbox { min-height: 0; overflow: hidden; }
  /* The delay lives on the open state only, so a pointer crossing the rail on
     its way somewhere else does not leave a wake of opening rows, while pulling
     off a row closes it at once.

     The keyboard branch is :focus-within qualified by :focus-visible, and the
     qualifier is load-bearing rather than decorative. Selecting a row focuses
     its link, by design, so a bare :focus-within would leave the clicked row
     open for as long as it held focus: 184px of art wedged into the rail after
     every click, with every row below it displaced. Measured in Chrome: a real
     mouse click leaves :focus true and :focus-visible false, while Tab and the
     arrow keys set both. So this opens for the keyboard, which has no other way
     to see the art, and stays out of the way of the mouse, which has hover. */
  .sr-rail[data-peek] .sr-row:hover > .sr-peek,
  .sr-rail[data-peek] .sr-row:focus-within:has(.sr-rowlink:focus-visible) > .sr-peek {
    grid-template-rows: 1fr; opacity: 1; transition-delay: .09s;
  }
  /* A row can hold focus while the pointer is somewhere else in the rail: tab to
     one, then reach for another. Left alone, a focused row ABOVE the hovered one
     opens, shoves the hovered row out from under the cursor, which closes it,
     which shoves it back.
     The `~ .sr-row:hover` is what makes that the only case this suppresses. The
     rule used to read :has(.sr-row:hover) on the RAIL, which is any row at all,
     and a row spans the full rail width with 4px between rows, so a mouse parked
     anywhere on the list killed the keyboard's peek outright: arrow through the
     rail with the pointer resting on it and no art ever opened. Now the focused
     row has to be ABOVE the hovered one, which is the only arrangement where
     opening it moves anything under the cursor. Browsers without :has() drop
     this rule and get the old behaviour, not a broken one. */
  .sr-rail[data-peek] .sr-row:not(.is-on):focus-within:not(:hover):has(~ .sr-row:hover) > .sr-peek {
    grid-template-rows: 0fr; opacity: 0; transition-delay: 0s;
  }
  /* The selected row holds its art open for as long as it is selected. Tony's
     call, and it is the reason the rail and the pane are not saying the same
     thing twice: the art in the rail marks WHICH row you are reading, the pane
     on the right is what you are reading. Selecting another row closes this one
     because only one row is ever .is-on, and deselecting closes it outright.
     Last in the cascade on purpose, so nothing above can shut it. */
  .sr-rail[data-peek] .sr-row.is-on > .sr-peek {
    grid-template-rows: 1fr; opacity: 1; transition-delay: 0s;
  }
  .sr-peekart {
    display: block; width: 100%; height: auto; aspect-ratio: 2 / 1;
    margin-top: 4px; object-fit: cover;
    border: 1px solid var(--line); border-radius: 7px; background: var(--key-face);
  }
  /* Same letterbox call as the plugin page: cropping a 16:9 slide to 2:1 eats
     its title. */
  .sr-peekart.ratio-16-9 { aspect-ratio: 16 / 9; object-fit: contain; background: #0b0d10; }
}

/* Held: the row the detail pane is showing. The product's own accent rides the
   whole border, which is the only place a row carries color. */
.sr-row.is-on .sr-rowlink {
  background: var(--panel);
  border-color: var(--accent); border-left-color: var(--accent);
}
.sr-row.is-on .sr-rowname { color: var(--paper); font-weight: 600; }
.sr-row.is-on .sr-rowtag { color: var(--paper); }
/* The global focus ring is teal, and on the selected row it drew a teal
   rectangle 2px outside a border already painted in the product's accent: two
   colors, 2px apart, saying the same thing. Keyboard focus on the held row wears
   the accent instead, so the ring reads as the same object as the border. Every
   other row keeps the teal, which is what tells you focus has left the selection. */
.sr-row.is-on .sr-rowlink:focus-visible { outline-color: var(--accent); }

/* -------------------------------------------------------- the detail pane */

/* The pane is its own scroll region, and a scroll region inherits the page's
   color-scheme, which is the browser default of light. Chrome then painted a
   16px light-grey scrollbar down the inside edge of a #161b21 panel, the one
   bright vertical line on the page. Scoped to this element, because it is the
   only nested scroller on the site that is dark on the inside; the page
   scrollbar is the browser's and stays that way. */
.sr-detail {
  position: sticky; top: 88px; align-self: start;
  max-height: calc(100vh - 108px); overflow-y: auto;
  padding: 20px 22px;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-top: 3px solid var(--accent); border-radius: 10px;
  color-scheme: dark;
  scrollbar-width: thin;
  scrollbar-color: var(--line) var(--bg-raised);
}
/* The resting pane. Nothing is selected at rest and nothing ever selects itself,
   so this is what the right column says for as long as the visitor is still
   reading the list: the line's own numbers, counted off src/plugins.mjs at build
   time, and how the page works. It replaced a single hint card that repeated the
   second lede almost word for word and left the column 122px tall beside a
   1,877px rail. */
.sr-d-hint { color: var(--slate); max-width: 46ch; font-size: .92rem; }
.sr-d-hint + .sr-d-hint { margin-top: 10px; }
.sr-d-resthead {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--slate); margin: 0 0 12px;
}
.sr-d-resthead ~ .sr-d-resthead {
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line);
}
.sr-d-stats { margin-top: 0; margin-bottom: 4px; }
.sr-d-stats .spec-n { font-size: 1.3rem; }
.sr-d-head { display: flex; gap: 14px; align-items: center; }
.sr-d-art {
  width: 64px; height: 64px; border-radius: 10px; flex: none;
  background: var(--key-face);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 2px 6px rgba(0, 0, 0, .45);
}
.sr-d-id { min-width: 0; }
.sr-d-meta {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-family: var(--font-mono); font-size: .66rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 3px;
}
.sr-d-name { margin-bottom: 0; font-size: 1.25rem; }
.sr-d-name a { color: var(--paper); }
.sr-d-name a:hover, .sr-d-name a:focus-visible { color: var(--hover); text-decoration: none; }
.sr-d-line { font-size: .92rem; max-width: 56ch; margin-top: 12px; }
.sr-d-specs { margin-top: 14px; gap: 7px; }
.sr-d-specs li { padding: 6px 12px; min-width: 0; }
.sr-d-specs .spec-n { font-size: 1.15rem; }
.sr-d-price {
  font-family: var(--font-display); font-weight: 800; font-size: 2rem; line-height: 1;
  color: var(--paper); margin: 18px 0 12px;
}
.sr-d-cta { display: flex; gap: 10px; flex-wrap: wrap; }
.sr-d-cta .btn { padding: 11px 18px; font-size: .74rem; }
.sr-d-note { margin-top: 12px; font-size: .8rem; color: var(--slate); max-width: 52ch; }

/* Every slide the plugin ships, stacked at the pane's full width. The pane is
   already a scroll region (max-height plus overflow-y above), so the strip
   lengthens the PANE and not the page: the rail beside it keeps its own height
   and nothing below the fold moves.

   Two columns, which is a REVERSAL of the full-width argument that shipped with
   this strip, made on the measurement that argument did not account for. Full
   width put ~3,360px of slides inside a 790px scroller, so a wheel over the
   right column scrolled slides for more than four screens before the page
   underneath it moved at all: a nested scroller that long stops reading as part
   of the page and starts reading as a trap. Two columns roughly halves it.

   What it costs is real and worth stating: these are 1920px information graphics
   with body copy baked in, and at two columns that copy is texture rather than
   text. It was already only 36% scale at full width, so the copy was never the
   job here; the strip is a contact sheet of what the listing shows, and every
   figure links to the full file. If Tony wants the readable width back, this is
   one rule and its own commit.

   Slide 01 never appears here. It is the Marketplace cover and it carries the
   name, the spec chips and the tagline in the art, all three of which this pane
   has already printed as text. It is the art the hovered rail row opens onto
   instead, where nothing surrounds it to repeat. */
.sr-d-galhead {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--slate);
  margin: 20px 0 10px; padding-top: 16px; border-top: 1px solid var(--line);
}
.sr-d-shots { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

/* Narrow screens. A left rail is not a rail at 390px, so the grid collapses to
   one column and the detail pane stops sticking outright. At this width the
   script moves the single detail node INTO the selected row, so it opens in
   flow directly beneath the row that was tapped and pushes the rest of the list
   down. Nothing is layered over anything at any scroll position, and with
   nothing selected the pane is not on the page at all: the resting state is the
   list and only the list.

   The chip rows become one scrolling rail each. Wrapped, nine field chips with
   labels as long as "Desktop and devices" stack four rows deep. A rail is the
   pattern every phone already uses, and the mask on the trailing edge is what
   says there is more to the right. */
@media (max-width: 900px) {
  .sr-browse { display: block; }
  .sr-detail {
    position: static; max-height: none; overflow: visible;
    margin: 6px 0 12px; padding: 16px 16px 18px;
  }
  .sr-d-price { font-size: 1.7rem; margin: 14px 0 10px; }
  /* The pane is inside the row here and it is the page that scrolls, so a
     stacked gallery would push the rest of the list a full screen and a half
     down for every product opened. Sideways instead: the slides become one
     swipeable strip, the next one showing at the edge so there is no doubt more
     exist. Same move the chip rows make just above, for the same reason. */
  .sr-d-shots {
    display: flex; flex-direction: row; overflow-x: auto; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory; gap: 10px; padding-bottom: 4px;
    scrollbar-width: none;
  }
  .sr-d-shots::-webkit-scrollbar { display: none; }
  .sr-d-shots > .shot { flex: 0 0 84%; scroll-snap-align: start; }

  /* Nine field chips with labels as long as "Desktop and devices" wrap two and
     three rows deep. A rail is the pattern every phone already uses, and the
     mask on the trailing edge is what says there is more to the right.
     Raised from 760 to 900 in the same pass that added the hover preview: 760
     left a ragged two-row wrap sitting across the whole tablet band, and this
     page already changes shape at 900, so one threshold now does both. */
  .sr-tools .cx-filters {
    flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain;
    padding-bottom: 3px; scrollbar-width: none;
  }
  /* The fade IS the signal, so it says something true or it does not appear.
     The mask used to be unconditional, which faded the last of the three
     platform chips on every screen they already fit on, as if something sat
     past them. showroom.js sets data-more when a row has more to scroll to and
     clears it at the end of the travel. */
  .sr-tools .cx-filters[data-more] {
    -webkit-mask-image: linear-gradient(90deg, #000 86%, transparent);
    mask-image: linear-gradient(90deg, #000 86%, transparent);
  }
  .sr-tools .cx-filters::-webkit-scrollbar { display: none; }
  .sr-tools .cx-chip { flex: none; }
}
/* Three buttons side by side stop fitting here, and a wrapped row of pill
   buttons at three different widths reads as debris. One per line instead. */
@media (max-width: 560px) {
  .sr-d-cta { display: grid; grid-template-columns: 1fr; gap: 8px; }
  .sr-d-cta .btn { text-align: center; padding: 11px 14px; font-size: .68rem; }
  /* Four chips of four different widths wrapped 3 and 1 in a 309px column at
     390, which left "6 PROFILES" alone on a line looking like an afterthought
     rather than the fourth of four. Two even columns instead: the chips are a
     set and they read as one. The three-chip products land 2 and 1, which is
     what an odd count looks like either way. */
  .sr-d-specs, .sr-d-stats { display: grid; grid-template-columns: 1fr 1fr; }
  .sr-d-specs li, .sr-d-stats li { padding: 5px 10px; min-width: 0; }
}

/* The store head on a phone. At 390x844 the first product row started at 772px,
   which is a store whose entire stock is below the fold: a visitor had to
   scroll past a full screen of preamble to learn that this page is a list of
   things to buy. The lede and the banner both stay, because the 35 countries
   are the reason this page exists and the banner is the honest caveat; they are
   phone-sized instead of desktop-sized. With the head trim above, four rows are
   on screen at rest. */
@media (max-width: 640px) {
  .sr-banner {
    margin-top: 16px; padding: 11px 13px; font-size: 0.86rem; line-height: 1.5;
  }
  /* The section rule's 56px of top padding sat between the banner and the
     search field, doing the work of a chapter break inside one screen. */
  .sr { padding-top: 22px; }
  .sr-tools { padding-bottom: 12px; }
  .sr-count { margin-top: 10px; }
}
