/* Generated by scripts/build_public_assets.py; do not edit directly. */

/* Source: web/static/css/tokens.css */
/* Design tokens — see docs/archive/DESIGN_SPEC_2026-05-15.md §2.
   Framework-agnostic CSS custom properties. Touch this file, not values
   in component stylesheets. */

/* Self-hosted fonts. Files live in /web/static/fonts/, sourced from
   @fontsource (Inter SIL OFL, JetBrains Mono Apache 2.0). Self-hosted
   because Google Fonts was blocked by browser ORB in some environments
   and h1 was falling back to the system stack. font-display: swap so
   the system fallback paints immediately and the woff2 hot-swaps in. */
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/inter-400.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/inter-500.woff2") format("woff2");
}
@font-face {
  font-family: "Inter";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/inter-600.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/jetbrains-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/jetbrains-mono-500.woff2") format("woff2");
}

:root {
  /* Surface — warm paper */
  --surface-page: #faf9f7;
  --surface-card: #fffdfb;
  --surface-card-nested: #fdfcfa;
  --surface-rail: #f5f3f0;

  /* Ink — text */
  --ink-1: #2b2823;
  --ink-2: #4a443e;
  --ink-3: #6b6660;
  --ink-4: #8b847c;

  /* Border */
  --border-subtle: #ebe7e0;
  --border-strong: #d4cfc6;

  /* --accent is the brand teal. Bright accents fail AA for body links
     on paper — use --accent-hover for any underlined text. Reserve
     --accent for icons, checked states, accent bars, primary chart lines.

     --accent-link is the *semantic* color for link text. It re-points
     per theme: on paper it's --accent-hover (the darker shade that
     passes AA on cream surfaces); in dark mode it flips to --accent
     (the brighter shade that passes AA on a dark surface). Components
     should reference --accent-link, not --accent-hover, for link text. */
  --accent: #1eb797;
  --accent-hover: #178f75;
  --accent-link: var(--accent-hover);

  /* Semantic / data colors */
  --data-positive: #1eb797;
  --data-attention: #ef7f1a;
  --data-negative: #b23b3b;
  --data-neutral: #6d7698;
  --data-used: #b07a4b;

  /* Typography */
  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo,
    monospace;

  --text-display: 24px;
  --text-display-line: 1.25;
  --text-display-weight: 600;

  --text-h2: 16px;
  --text-h2-line: 1.4;
  --text-h2-weight: 600;

  --text-h3: var(--text-body);

  --text-body: 13px;
  --text-body-line: 1.5;
  --text-body-weight: 400;

  --text-ui: 11px;
  --text-ui-line: 1.4;
  --text-ui-weight: 500;

  --text-meta: 10px;
  --text-meta-line: 1.4;
  --text-meta-weight: 400;

  /* Spacing — 8px base, tight rhythm per spec §2.3 */
  --space-1: 2px;
  --space-2: 4px;
  --space-3: 6px;
  --space-4: 8px;
  --space-5: 12px;
  --space-6: 16px;
  --space-8: 20px;
  --space-10: 24px;
  --space-12: 32px;

  /* Border & shape — square is the brand */
  --radius-0: 0;
  --radius-tip: 2px;
  --hairline: 1px;

  /* Motion */
  --transition-default: 120ms ease-out;

  /* Elevation — only tooltips/hover-cards get a shadow */
  --shadow-tip: 0 4px 12px rgba(43, 40, 35, 0.08);

  /* Focus ring — accent at 30% opacity per spec §5 */
  --focus-ring: 0 0 0 2px rgba(30, 183, 151, 0.3);

  /* Paper noise — dark grain on light surface. Body references
     --noise-image so the theme can swap it for a light-on-dark variant
     without re-declaring the background-image rule. */
  --noise-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.17 0 0 0 0 0.16 0 0 0 0 0.14 0 0 0 0.15 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* ---------- dark paper ----------
   Warm-dark variant. Surfaces are deep brown (think a well-bound book
   under low light, not a tech dashboard); ink is warm cream. Accent
   values stay the same hex — the bright versions read well on dark —
   but --accent-link flips to point at --accent rather than
   --accent-hover, because the *darker* hover shade fails AA on dark
   surfaces.

   Toggled via [data-theme="dark"] on <html>. The default is dark
   (set as an HTML attribute so first paint is correct); a tiny inline
   script in <head> reads localStorage and switches to light if the
   visitor has chosen it, before stylesheets apply. */
[data-theme="dark"] body {
  background-blend-mode: soft-light;
}

[data-theme="dark"] {
  --accent-link: var(--accent);

  --surface-page: #1a1612;
  --surface-card: #221c16;
  --surface-card-nested: #1d1813;
  --surface-rail: #14110d;

  --ink-1: #f4ecdc;
  --ink-2: #cdc3b1;
  --ink-3: #97907f;
  --ink-4: #6e6859;

  --border-subtle: #2a241c;
  --border-strong: #463c30;

  --shadow-tip: 0 4px 12px rgba(0, 0, 0, 0.55);

  --noise-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.96 0 0 0 0 0.92 0 0 0 0 0.82 0 0 0 0.30 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --transition-default: 0ms;
  }
}

/* Source: web/static/css/base.css */
/* Base shell — see docs/archive/DESIGN_SPEC_2026-05-15.md §3, §4.
   Tokens live in tokens.css. Component styling lives in components/. */

/* ---------- reset ---------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

/* Skip link — first focusable element in body. Hidden offscreen
   until focused, then jumps into view so keyboard / screen-reader users
   can bypass the header and nav. Targets #main-content in base.html.jinja. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  padding: var(--space-2) var(--space-4);
  background: var(--ink-2);
  color: var(--surface-page);
  text-decoration: none;
  font-weight: 600;
  z-index: 1000;
  transform: translateY(-110%);
  transition: transform 0.12s ease;
}
.skip-link:focus,
.skip-link:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--text-body-line);
  color: var(--ink-2);
  background-color: var(--surface-page);
  /* Paper noise on the page surface only — cards/tables intentionally
     cover it. Light theme uses dark grain, dark uses cream grain.
     Defined in tokens.css. Blend mode keeps the warm chroma intact
     (straight alpha blending muddied everything toward gray): multiply
     for light so dark grain just darkens cream; the dark theme
     overrides to screen so cream grain lightens the warm brown. */
  background-image: var(--noise-image);
  background-blend-mode: multiply;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

img {
  max-width: 100%;
  display: block;
}

[hidden] {
  display: none !important;
}

/* ---------- typography ---------- */
h1,
h2,
h3,
h4 {
  color: var(--ink-1);
  margin: 0;
  font-weight: var(--text-h2-weight);
  letter-spacing: -0.005em;
}

h1 {
  font-size: var(--text-display);
  line-height: var(--text-display-line);
  font-weight: var(--text-display-weight);
}

h2 {
  font-size: var(--text-h2);
  line-height: var(--text-h2-line);
}

h3 {
  font-size: var(--text-body);
  line-height: var(--text-body-line);
  font-weight: 600;
}

p {
  margin: 0;
  color: var(--ink-2);
}

ul,
ol {
  margin: 0;
  padding-left: var(--space-8);
  color: var(--ink-2);
}

li + li {
  margin-top: var(--space-2);
}

strong {
  color: var(--ink-1);
  font-weight: 600;
}

em {
  color: var(--ink-2);
}

/* Mono numerics — tabular figures per spec §2.2 */
code,
kbd,
samp,
.mono,
.num {
  font-family: var(--font-mono);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  color: var(--ink-1);
}

/* Eyebrow / UI label — 11px uppercase tracking-wide */
.eyebrow {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.muted {
  color: var(--ink-3);
  font-size: var(--text-body);
}

.meta {
  color: var(--ink-3);
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
}

/* ---------- links ---------- */
a {
  color: var(--accent-link);
  text-decoration: none;
  border-bottom: var(--hairline) solid transparent;
  transition: color var(--transition-default),
    border-color var(--transition-default);
}

a:hover {
  color: var(--ink-1);
  border-bottom-color: var(--ink-1);
}

a:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Source: web/static/css/layout.css */
/* ---------- page shell ---------- */
.page {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-10) var(--space-12);
}

/* ---------- breadcrumb ---------- */
.crumb {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  color: var(--ink-3);
  margin: 0 0 var(--space-6);
  letter-spacing: 0.02em;
}

.crumb a {
  color: var(--ink-3);
  border-bottom-color: transparent;
}

.crumb a:hover {
  color: var(--ink-1);
  border-bottom-color: var(--ink-1);
}

.crumb-sep {
  color: var(--ink-4);
  margin: 0 var(--space-3);
}

/* ---------- content rhythm ----------
   Public templates wrap {% block content %} in <main>, so these rules
   target main's children rather than .page's direct children (header,
   main, footer). Without the extra hop the rhythm rules fire on the
   <main> wrapper only and never reach the H1/H2/p/ul inside it. */
.page > main > * + * {
  margin-top: var(--space-8);
}

.page > main > h1 + .muted,
.page > main > h1 + p.muted {
  margin-top: var(--space-3);
}

.page > main > h2 {
  margin-top: var(--space-10);
}

.page > main > p,
.page > main > ul,
.page > main > ol {
  margin-top: var(--space-5);
}

/* Source: web/static/css/components/header.css */
.site-header {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  padding: var(--space-6) 0 var(--space-8);
  border-bottom: var(--hairline) solid var(--border-subtle);
  margin-bottom: var(--space-10);
}

.brand {
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--ink-1);
  letter-spacing: -0.01em;
  border-bottom: none;
}

.brand:hover {
  color: var(--accent-link);
  border-bottom: none;
}

.brand-mark {
  /* Logo symbol before the wordmark. The square fill is bound to
     --accent (the brand teal, constant across themes) so the mark reads
     the same in light and dark. Footer overrides the size smaller. */
  display: inline-block;
  width: 20px;
  height: 20px;
  margin-right: var(--space-3);
  vertical-align: middle;
  position: relative;
  top: -2px;
}

.site-nav {
  display: flex;
  gap: var(--space-8);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Hamburger toggle — shown only on narrow viewports (display flipped in
   the mobile media query). Square icon button so it reads as the
   standard mobile-menu glyph; sized to clear the 44px tap-target floor. */
.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-3);
  padding: 0;
  cursor: pointer;
}

.nav-toggle:hover {
  color: var(--ink-1);
  border-color: var(--ink-1);
}

.nav-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.nav-toggle__icon {
  display: block;
}

.site-nav a {
  color: var(--ink-3);
  border-bottom-color: transparent;
}

.site-nav a:hover,
.site-nav a[aria-current="page"] {
  color: var(--ink-1);
  border-bottom-color: var(--ink-1);
}

/* ---------- header search ----------
   Lives between the nav and the theme toggle on desktop, drops to its
   own row on mobile (handled by the existing .site-header flex-wrap).
   The keybind hint and dropdown are rendered inline so the script
   doesn't need to inject any chrome at runtime. */
.site-search {
  position: relative;
  flex: 0 1 240px;
  margin-left: auto;
}

.site-search__input {
  width: 100%;
  height: 32px;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0 var(--space-8) 0 var(--space-5);
}

.site-search__input::placeholder {
  color: var(--ink-4);
}

.site-search__input:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-color: var(--accent);
}

/* "/" hint sits inside the input, hidden once focused so it doesn't
   collide with what the user is typing. */
.site-search__hint {
  position: absolute;
  right: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: var(--text-meta);
  color: var(--ink-3);
  background-color: var(--surface-rail);
  border: var(--hairline) solid var(--border-subtle);
  padding: 0 var(--space-2);
  pointer-events: none;
}

.site-search__input:focus + .site-search__hint {
  display: none;
}

/* Dropdown — anchored to the input, paper aesthetic, square edges. */
.site-search__results {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  /* Anchored to the input's right edge and allowed to grow leftward so
     plugin names get real room — the input itself is deliberately narrow,
     but a 240px dropdown squeezed names to nothing. */
  min-width: 340px;
  max-width: 90vw;
  list-style: none;
  padding: 0;
  margin: 0;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  box-shadow: var(--shadow-tip);
  z-index: 10;
  max-height: 60vh;
  overflow-y: auto;
}

.site-search__results li + li {
  margin-top: 0;
  border-top: var(--hairline) solid var(--border-subtle);
}

.site-search__opt a {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  color: var(--ink-1);
  border-bottom: none;
}

.site-search__opt.is-active a,
.site-search__opt a:hover {
  background-color: var(--surface-rail);
  color: var(--ink-1);
}

/* Type pill — distinguishes plugin vs. brand vs. category at a glance
   so a query like "reverb" doesn't blur the categories together with
   the plugin names. Uppercase, hairline, muted; sits left of the name
   so the eye picks the verdict before reading. */
.site-search__kind {
  flex: 0 0 auto;
  font-size: 9px;
  line-height: 1.4;
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  background-color: var(--surface-rail);
  border: var(--hairline) solid var(--border-subtle);
  padding: 1px var(--space-2);
  min-width: 56px;
  text-align: center;
}

.site-search__opt[data-type="manufacturer"] .site-search__kind {
  color: var(--accent-link);
  border-color: color-mix(in srgb, var(--accent) 25%, transparent);
}

.site-search__name {
  font-weight: 600;
  /* The name is the point of the row — it keeps its intrinsic width and
     the manufacturer subtitle is the element that shrinks/truncates. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-search__mfr {
  color: var(--ink-3);
  font-size: var(--text-meta);
  flex: 1 1 auto;
  min-width: 0;
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-search__empty {
  padding: var(--space-3) var(--space-5);
  color: var(--ink-3);
  font-size: var(--text-meta);
}

/* /search results page sections — quiet rows of links, same restraint
   as the typeahead dropdown but visible all at once. */
.search-results {
  list-style: none;
  padding-left: 0;
  margin: 0 0 var(--space-6) 0;
}

.search-results__item {
  padding: var(--space-2) 0;
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.search-results__item a {
  font-weight: 600;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Theme toggle — icon-only square button on the far right of the nav.
   Shows the icon for the *target* theme (sun in dark mode, moon in light
   mode) so the action it triggers is unambiguous. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  width: 32px;
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-3);
  padding: 0;
  cursor: pointer;
  transition: color var(--transition-default),
    border-color var(--transition-default),
    background-color var(--transition-default);
}

.theme-toggle:hover {
  color: var(--ink-1);
  border-color: var(--ink-1);
}

.theme-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.theme-toggle__icon--moon { display: none; }
[data-theme="light"] .theme-toggle__icon--sun { display: none; }
[data-theme="light"] .theme-toggle__icon--moon { display: inline-block; }

/* Source: web/static/css/components/watchlist.css */
/* ---------- watchlist ----------
   Per-account price tracking. The header count and the plugin-detail
   "track price" card reuse the site's squared-off form/button language
   rather than introducing a modal or a separate app surface. */
.watchlist-toggle {
  height: 32px;
  display: inline-flex;
  align-items: center;
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0 var(--space-5);
  cursor: pointer;
  transition: color var(--transition-default),
    border-color var(--transition-default),
    background-color var(--transition-default);
}

.watchlist-toggle:hover,
.watchlist-toggle[aria-expanded="true"] {
  color: var(--ink-1);
  border-color: var(--ink-1);
}

.watchlist-toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.watchlist-toggle__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5em;
  margin-left: var(--space-2);
  padding: 0 var(--space-2);
  background-color: color-mix(in srgb, var(--accent) 12%, transparent);
  border: var(--hairline) solid color-mix(in srgb, var(--accent) 24%, transparent);
  color: var(--accent-link);
  font-family: var(--font-mono);
  letter-spacing: 0;
}

.watchlist-panel__toast {
  /* Fixed overlay so the message is visible regardless of scroll
     position — the bulk bar at the bottom of the catalog fires this
     from a row well down the page. Z-index sits one above the bulk
     bar so the toast stacks cleanly above it. */
  position: fixed;
  bottom: calc(var(--space-5) + 56px);
  left: 50%;
  transform: translateX(-50%);
  max-width: min(420px, calc(100vw - var(--space-6) * 2));
  padding: var(--space-3) var(--space-5);
  background-color: color-mix(in srgb, var(--accent) 18%, var(--surface-card));
  border: var(--hairline) solid color-mix(in srgb, var(--accent) 40%, transparent);
  color: var(--ink-1);
  font-size: var(--text-ui);
  box-shadow: 0 8px 24px color-mix(in srgb, #000 30%, transparent);
  z-index: 51;
}

.watchlist-panel__toast[hidden] { display: none; }

/* The "Track price" and "Submit report" buttons on the plugin-detail
   action panel. The old standalone .watchlist-card container/copy
   selectors were retired when these buttons moved into .action-panel;
   only the button styling below is still in use. */
.watchlist-card__toggle,
.watchlist-card__save {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: background-color var(--transition-default),
    border-color var(--transition-default),
    color var(--transition-default);
}

.watchlist-card__toggle {
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-2);
}

.watchlist-card__save {
  background-color: var(--accent);
  border: var(--hairline) solid var(--accent);
  color: var(--surface-card);
}

[data-theme="dark"] .watchlist-card__save {
  color: var(--surface-page);
}

.watchlist-card__toggle:hover {
  background-color: var(--surface-rail);
  border-color: var(--ink-3);
  color: var(--ink-1);
}

.watchlist-card__toggle[aria-pressed="true"] {
  background-color: var(--surface-rail);
  border-color: var(--ink-1);
  color: var(--ink-1);
}

.watchlist-card__save:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
}

.watchlist-card__toggle:focus-visible,
.watchlist-card__save:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Source: web/static/css/components/auth-gate.css */
/* ---------- auth gate (contextual sign-in / sign-up modal) ----------
   Shown when a logged-out visitor taps a gated action (Track price,
   Favorite, Recommend) instead of teleporting them to a bare /login.
   Built on the native <dialog> element so we inherit focus-trapping,
   Escape-to-close, and an inert background for free; the JS only fills
   in the contextual copy and the ?next= links.

   Square is the brand: no rounded corners, a single accent left-border
   like the action/buy cards on the detail page. */
.auth-gate {
  width: min(26rem, calc(100vw - var(--space-6) * 2));
  padding: 0;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-left: 4px solid var(--accent);
  color: var(--ink-1);
  box-shadow: 0 12px 40px color-mix(in srgb, #000 35%, transparent);
}

.auth-gate::backdrop {
  background-color: color-mix(in srgb, var(--surface-page) 72%, transparent);
}

.auth-gate__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-6);
}

.auth-gate__close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: transparent;
  border: 0;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--transition-default);
}

.auth-gate__close:hover {
  color: var(--ink-1);
}

.auth-gate__close:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.auth-gate__brand {
  display: inline-flex;
}

.auth-gate__title {
  margin: 0;
  font-size: var(--text-display);
  line-height: var(--text-display-line);
  font-weight: var(--text-display-weight);
}

.auth-gate__body {
  margin: 0;
  font-size: var(--text-body);
  line-height: var(--text-body-line);
  color: var(--ink-2);
}

.auth-gate__perks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.auth-gate__perks li {
  position: relative;
  padding-left: var(--space-6);
  font-size: var(--text-ui);
  line-height: 1.45;
  color: var(--ink-2);
}

/* Square accent tick, in keeping with the brand mark. */
.auth-gate__perks li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.3em;
  width: 8px;
  height: 8px;
  background-color: var(--accent);
}

.auth-gate__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-2);
}

.auth-gate__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-4) var(--space-6);
  background-color: var(--accent);
  border: var(--hairline) solid var(--accent);
  color: var(--surface-card);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-align: center;
  text-decoration: none;
  transition: background-color var(--transition-default),
    border-color var(--transition-default);
}

.auth-gate__cta:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--surface-card);
}

[data-theme="dark"] .auth-gate__cta {
  color: var(--surface-page);
}

[data-theme="dark"] .auth-gate__cta:hover {
  color: var(--surface-page);
}

.auth-gate__alt {
  margin: 0;
  font-size: var(--text-ui);
  line-height: 1.45;
  color: var(--ink-3);
  text-align: center;
}

.auth-gate__alt span,
a.auth-gate__alt {
  color: var(--accent-link);
}

.auth-gate__alt--note {
  text-align: left;
}

/* Source: web/static/css/components/footer.css */
.site-footer {
  margin-top: var(--space-12);
  padding-top: var(--space-10);
  border-top: var(--hairline) solid var(--border-subtle);
  color: var(--ink-3);
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
}

.site-footer__cols {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr 1fr;
  gap: var(--space-8);
  padding-bottom: var(--space-8);
}

.site-footer__col ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--space-2);
}

.site-footer__col li {
  margin: 0;
}

.site-footer__brand {
  display: inline-flex;
  align-items: center;
  font-size: var(--text-h3);
  font-weight: 600;
  color: var(--ink-1);
  letter-spacing: -0.01em;
  border-bottom: none;
  margin-bottom: var(--space-3);
}

.site-footer__brand:hover {
  color: var(--accent-link);
  border-bottom: none;
}

/* Smaller logo mark to match the footer's smaller wordmark. The footer
   brand is flex-aligned, so drop the optical `top` nudge the header uses. */
.site-footer__brand .brand-mark {
  width: 16px;
  height: 16px;
  top: 0;
}

.site-footer__tagline {
  margin: 0;
  max-width: 32ch;
}

.site-footer__title {
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-2);
  margin: 0 0 var(--space-3);
}

.site-footer__base {
  padding: var(--space-5) 0;
  border-top: var(--hairline) solid var(--border-subtle);
}

.site-footer p {
  color: var(--ink-3);
  margin: var(--space-2) 0;
}

.site-footer a {
  color: var(--ink-3);
}

.site-footer a:hover {
  color: var(--ink-1);
}

@media (max-width: 720px) {
  .site-footer__cols {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 480px) {
  .site-footer__cols {
    grid-template-columns: 1fr;
  }
}

/* Source: web/static/css/components/catalog.css */
/* ---------- plugin row thumb ----------
   The image itself is now the shared .gui-tile component, sized by the
   .gui-tile--thumb modifier (components/gui-tile.css). The cell still
   reserves the column width. */
.thumb-cell {
  width: 88px;
  padding: var(--space-3);
}

/* Manufacturer support block — same plain-and-reference treatment as
   the license panel on plugin detail pages. Only renders when at
   least one of the three fields is populated. */
.support-block {
  margin: var(--space-8) 0;
  padding: var(--space-6) 0;
  border-top: var(--hairline) solid var(--border-subtle);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.support-block__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
}

.support-block__row dt {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.support-block__row dd {
  margin: 0;
  color: var(--ink-2);
  word-break: break-word;
}

/* Bulk-select checkbox column on the shared plugin table. Narrow
   fixed column so the rest of the table layout doesn't reflow. */
.col-select {
  width: 36px;
  padding-left: var(--space-3);
  vertical-align: middle;
}

.bulk-select {
  width: 18px;
  height: 18px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

/* Compare entry point above the catalog table. The one-click CTA is the
   primary discovery affordance for side-by-side compare (the row
   checkboxes alone never got found, and they're hidden on mobile). */
.compare-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}

.compare-bar__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  transition: color var(--transition-default),
    border-color var(--transition-default);
}

.compare-bar__cta:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.compare-bar__cta:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-color: var(--accent);
}

.compare-bar__icon {
  flex: 0 0 auto;
  color: var(--accent);
}

.compare-bar__hint {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

/* The hint teaches the checkbox flow, which only exists on desktop —
   on mobile the bulk-select column is dropped, so drop the hint too and
   leave just the one-click CTA. */
@media (max-width: 1023px) {
  .compare-bar__hint {
    display: none;
  }
}

/* Sortable column header on the plugin list. Wraps the header label in
   an anchor; the arrow ↑/↓ reflects the current direction. Inherits the
   header typography so it doesn't stand out from non-sortable headers
   when inactive. */
.plugin-list__sort {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: inherit;
  border-bottom: none;
  cursor: pointer;
}

.plugin-list__sort:hover {
  color: var(--ink-1);
  border-bottom: none;
}

.plugin-list__sort--asc,
.plugin-list__sort--desc {
  color: var(--ink-1);
}

.plugin-list__sort-arrow {
  font-family: var(--font-mono);
  font-size: var(--text-meta);
  color: var(--accent-link);
}

/* Classic catalog pagination. Kept compact because it sits directly under
   the result table and needs to scan like table chrome, not a new panel. */
.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

.pagination__pages {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0;
}

.pagination__pages > li {
  margin: 0;
}

.pagination__control,
.pagination__page,
.pagination__ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: var(--hairline) solid transparent;
  color: var(--ink-2);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  text-decoration: none;
}

.pagination__control,
.pagination__page {
  border-color: var(--border-strong);
}

.pagination__control:hover,
.pagination__page:hover {
  border-color: var(--accent);
  color: var(--accent-link);
}

.pagination__page--current {
  border-color: var(--accent);
  color: var(--ink-1);
  background-color: var(--surface-rail);
}

.pagination__control--disabled {
  border-color: var(--border-subtle);
  color: var(--ink-4);
}

.pagination__ellipsis {
  min-width: 24px;
  padding: 0;
  color: var(--ink-4);
}

/* Floating scroll-to-top — bottom-right, fades in once the visitor has
   scrolled past the first screen. Sits beside (not over) .bulk-bar
   because the bulk-bar is horizontally centered. */
.scroll-top {
  position: fixed;
  right: var(--space-5);
  bottom: var(--space-5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  cursor: pointer;
  box-shadow: 0 8px 24px color-mix(in srgb, #000 30%, transparent);
  z-index: 49;
}

.scroll-top:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.scroll-top:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-color: var(--accent);
}

.scroll-top[hidden] {
  display: none;
}

/* Sticky footer bar — appears when any row is selected. Floats above
   the page so it stays visible while the user scrolls a long list. */
.bulk-bar {
  position: fixed;
  left: 50%;
  bottom: var(--space-5);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-5);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  box-shadow: 0 8px 24px color-mix(in srgb, #000 30%, transparent);
  z-index: 50;
}

.bulk-bar[hidden] {
  display: none;
}

.bulk-bar__count {
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--text-meta);
}

.bulk-bar__action {
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}

.bulk-bar__action:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent-link);
}

.bulk-bar__action:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.bulk-bar__action--quiet {
  color: var(--ink-3);
}

/* Inverted chrome so the floating selection bar pops against the page
   instead of blending into it: in light mode it wears the dark palette,
   in dark mode the light one. We re-point only the color tokens the bar
   consumes (custom properties inherit, so .bulk-bar__* pick these up)
   rather than hard-coding each rule — values mirror the dark/light
   blocks in tokens.css. --accent stays the same hex in both themes; only
   --accent-link flips. */
.bulk-bar {
  --surface-card: #221c16;
  --ink-1: #f4ecdc;
  --ink-2: #cdc3b1;
  --ink-3: #97907f;
  --border-strong: #463c30;
  --accent-link: var(--accent);
}

[data-theme="dark"] .bulk-bar {
  --surface-card: #fffdfb;
  --ink-1: #2b2823;
  --ink-2: #4a443e;
  --ink-3: #6b6660;
  --border-strong: #d4cfc6;
  --accent-link: var(--accent-hover);
}

/* ---------- browse columns (categories & tags page) ----------
   CSS multi-column flow so a 4-word category like "Channel Strip /
   Mastering AIO / Tape / Preamp" doesn't stretch every other cell in
   its row. Items stack vertically, browsers fill column 1 top-to-bottom
   before starting column 2 — which matches the alphabetical order so
   visual scanning is uninterrupted. */
.browse-grid {
  list-style: none;
  padding: 0;
  margin: var(--space-4) 0 var(--space-8);
  columns: 18rem auto;
  column-gap: var(--space-6);
}

.browse-grid > li {
  break-inside: avoid;
  /* Defeats the global li+li margin in a multi-column layout. */
  margin-top: 0;
}

.browse-grid > li + li {
  margin-top: 0;
}

.browse-grid__link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-1);
  border-bottom: var(--hairline) solid var(--border-subtle);
  color: var(--ink-1);
  text-decoration: none;
  transition: background-color var(--transition-default),
    color var(--transition-default);
}

.browse-grid__link:hover {
  background-color: var(--surface-rail);
  color: var(--accent-link);
}

.browse-grid__name {
  font-weight: var(--text-ui-weight);
}

.browse-grid__count {
  font-size: var(--text-meta);
  color: var(--ink-3);
  white-space: nowrap;
}

/* On-sale price: the lowest figure stays in the cell as the "now" price;
   this strip below it shows the struck list price and the discount so a
   deal is legible at a glance instead of hidden inside a normal-looking
   price. Rendered only when the lowest price is itself a sale. */
.price-sale {
  display: block;
  margin-top: 2px;
  font-size: var(--text-meta);
  line-height: 1.2;
  white-space: nowrap;
}

.price-was {
  color: var(--ink-4);
  text-decoration: line-through;
}

.price-off {
  margin-left: 4px;
  font-weight: 600;
  color: var(--accent-link);
}

/* Source: web/static/css/components/catalog-sidebar.css */
/* ---------- catalog two-column layout (sidebar + results) ---------- */
.catalog-layout {
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  gap: var(--space-8);
  align-items: start;
}

.catalog-main {
  min-width: 0;
}

/* The .page > main rhythm rules only reach main's direct children, so
   re-establish vertical spacing for the content column here. */
.catalog-main > * + * {
  margin-top: var(--space-6);
}

/* Mobile-only "Filters" button that opens the sidebar as a drawer. Hidden
   on desktop (responsive.css / the 720px block flips it on), where the
   sidebar is always in view. */
.catalog-filter-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
}

.catalog-filter-toggle__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 var(--space-2);
  background-color: var(--accent);
  color: #15110d;
  font-weight: 600;
}

/* ---------- left filter sidebar ---------- */
.catalog-sidebar {
  position: sticky;
  top: var(--space-6);
  align-self: start;
  max-height: calc(100vh - var(--space-10));
  overflow-y: auto;
  /* Scroll without a visible scrollbar — the sidebar still scrolls when
     facets overflow the viewport, but the rail isn't drawn. */
  scrollbar-width: none;
}

.catalog-sidebar::-webkit-scrollbar {
  display: none;
}

.catalog-sidebar__form {
  background-color: var(--surface-rail);
  border: var(--hairline) solid var(--border-strong);
  padding: var(--space-5);
}

.catalog-sidebar__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: var(--hairline) solid var(--border-strong);
}

.catalog-sidebar__title {
  margin: 0;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.catalog-sidebar__selected {
  font-family: var(--font-mono);
  font-size: var(--text-meta);
  color: var(--accent-link);
}

/* ---------- clear-all link ---------- */
.catalog-sidebar__clear-row {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-3);
}

.catalog-sidebar__clear {
  font-size: var(--text-meta);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  border-bottom: none;
}

.catalog-sidebar__clear:hover {
  color: var(--accent-link);
}

/* ---------- grouped facet filters (Sale / OS / Format / …) ---------- */
.filter-facets {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-5);
  border-bottom: var(--hairline) solid var(--border-strong);
}

.facet__title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ---------- collapsible group ---------- */
.filter-group + .filter-group {
  border-top: var(--hairline) solid var(--border-subtle);
}

.filter-group__header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.filter-group__check {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

.filter-group__check:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.filter-group__toggle {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-3) 0;
  background: transparent;
  border: none;
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  text-align: left;
  cursor: pointer;
}

.filter-group__toggle:hover {
  color: var(--ink-1);
}

.filter-group__toggle:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.filter-group__name {
  flex: 1 1 auto;
  min-width: 0;
}

.filter-group__count {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-meta);
  letter-spacing: 0;
  color: var(--ink-4);
}

.filter-group__chevron {
  flex: 0 0 auto;
  color: var(--ink-4);
  transition: transform var(--transition-default);
}

.filter-group.is-collapsed .filter-group__chevron {
  transform: rotate(-90deg);
}

.filter-group__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* Indent the types under their group, on a distinct inset panel so
     the hierarchy reads at a glance. */
  margin: 0 0 var(--space-3) var(--space-6);
  padding: var(--space-2);
  background-color: var(--surface-card);
  border-left: 2px solid var(--border-strong);
  border-radius: 0 2px 2px 0;
}

.filter-group__body[hidden] {
  display: none;
}

/* ---------- option row ---------- */
.filter-opt {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: 2px;
  cursor: pointer;
}

.filter-opt:hover {
  background-color: var(--surface-card-nested);
}

.filter-opt input {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
}

.filter-opt input:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.filter-opt__label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-body);
  line-height: var(--text-body-line);
  color: var(--ink-2);
}

.filter-opt input:checked ~ .filter-opt__label {
  color: var(--ink-1);
}

.filter-opt__count {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-meta);
  color: var(--ink-4);
}

.catalog-sidebar__apply {
  width: 100%;
  min-height: 40px;
  margin-top: var(--space-4);
  background-color: var(--accent);
  border: var(--hairline) solid var(--accent);
  color: var(--surface-card);
  font-family: inherit;
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
}

/* ---------- price range slider ---------- */
.filter-price {
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-5);
  border-bottom: var(--hairline) solid var(--border-strong);
}

.filter-price__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.filter-price__values {
  font-family: var(--font-mono);
  font-size: var(--text-body);
  color: var(--ink-1);
}

/* Two native range inputs stacked over a shared rail. The inputs are
   transparent and pointer-events:none; only their thumbs are
   interactive, so both handles stay grabbable on one track. JS paints
   the fill + value label and keeps the handles from crossing. */
.price-slider {
  position: relative;
  height: 22px;
}

.price-slider__rail,
.price-slider__fill {
  position: absolute;
  top: 50%;
  height: 4px;
  transform: translateY(-50%);
  border-radius: 2px;
  pointer-events: none;
}

.price-slider__rail {
  left: 9px;
  right: 9px;
  background-color: var(--border-strong);
}

.price-slider__fill {
  left: 9px;
  width: 0;
  background-color: var(--accent);
}

.price-slider__input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 22px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
}

.price-slider__input--min {
  z-index: 3;
}
.price-slider__input--max {
  z-index: 4;
}
.price-slider__input:focus {
  z-index: 5;
  outline: none;
}

.price-slider__input::-webkit-slider-runnable-track {
  background: transparent;
  border: none;
}
.price-slider__input::-moz-range-track {
  background: transparent;
  border: none;
}

.price-slider__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background-color: var(--accent);
  border: 3px solid var(--surface-rail);
  box-shadow: 0 0 0 1px var(--accent);
  cursor: pointer;
  pointer-events: auto;
}
.price-slider__input::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background-color: var(--accent);
  border: 3px solid var(--surface-rail);
  box-shadow: 0 0 0 1px var(--accent);
  cursor: pointer;
  pointer-events: auto;
}
.price-slider__input:focus-visible::-webkit-slider-thumb {
  box-shadow: var(--focus-ring);
}
.price-slider__input:focus-visible::-moz-range-thumb {
  box-shadow: var(--focus-ring);
}

/* ---------- narrow viewports: stack sidebar above results ---------- */
@media (max-width: 720px) {
  .catalog-layout {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }

  .catalog-filter-toggle {
    display: flex;
  }

  .catalog-sidebar {
    position: static;
    max-height: none;
    overflow: visible;
    /* Hidden until the Filters button opens it, so results lead. */
    display: none;
  }

  .catalog-sidebar.is-open {
    display: block;
  }

  /* WCAG touch targets on a phone. */
  .filter-group__toggle {
    min-height: 44px;
  }
  .filter-opt {
    padding: var(--space-3) var(--space-2);
  }
  .filter-opt input {
    width: 18px;
    height: 18px;
  }
}

/* Source: web/static/css/components/tables.css */
/* ---------- tables ---------- */
table {
  width: 100%;
  border-collapse: collapse;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
}

thead th {
  background-color: var(--surface-rail);
  border-bottom: var(--hairline) solid var(--border-strong);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-align: left;
  padding: var(--space-3) var(--space-5);
}

tbody th,
tbody td {
  padding: var(--space-5);
  border-bottom: var(--hairline) solid var(--border-subtle);
  vertical-align: top;
  color: var(--ink-2);
}

tbody tr:last-child th,
tbody tr:last-child td {
  border-bottom: none;
}

tbody th {
  background-color: var(--surface-card-nested);
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  width: 14rem;
}

tbody tr:hover td {
  background-color: var(--surface-card-nested);
}

/* Numeric columns / cells — mono with tabular figures */
td.num,
th.num,
.price {
  font-family: var(--font-mono);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  color: var(--ink-1);
}

.price-was {
  color: var(--ink-3);
  font-family: var(--font-mono);
  font-feature-settings: "tnum";
  font-variant-numeric: tabular-nums;
  margin-left: var(--space-3);
}

.price-context {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-1);
  color: var(--ink-3);
  font-family: var(--font-sans);
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
  font-variant-numeric: normal;
}

/* Source: web/static/css/primitives.css */
/* ---------- card (SectionCard base, spec §4.1) ---------- */
.card {
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  padding: var(--space-8);
}

.card--nested {
  background-color: var(--surface-card-nested);
  border-color: var(--border-subtle);
  padding: var(--space-5);
}

.card--dense {
  padding: var(--space-5);
}

.card__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  padding-bottom: var(--space-5);
  margin-bottom: var(--space-5);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.card__title {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.card__meta {
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
  color: var(--ink-3);
}

/* ---------- tag pill (spec §4.7) ---------- */
.tag {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  background-color: var(--surface-rail);
  border: var(--hairline) solid var(--border-subtle);
  color: var(--ink-2);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-radius: var(--radius-0);
  transition: background-color var(--transition-default),
    border-color var(--transition-default);
}

a.tag:hover {
  background-color: var(--surface-card);
  border-color: var(--border-strong);
  color: var(--ink-1);
}

.tag--accent {
  background-color: color-mix(in srgb, var(--accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--accent) 20%, transparent);
  color: var(--accent-link);
}

.tag--outline {
  background-color: var(--surface-card);
  border-color: var(--border-strong);
}

.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* Primary-category chip can render either its full name or its
   first-slash-segment short form. Desktop shows the full name; the
   plugin-list small-screen rule flips this so multi-part categories
   like "Saturation / Waveshaper / Clipper" stop wrapping to three
   lines on a phone. */
.tag__short {
  display: none;
}

/* Manufacturer cell on the plugin list: always linked but rendered
   in body color, not the accent link color. The teal reads as
   navigation everywhere else; here it competed with the plugin name
   for attention. Underline appears on hover so the affordance is
   still obvious. */
.col-mfr__link {
  color: var(--ink-2);
  border-bottom: none;
}

.col-mfr__link:hover {
  color: var(--ink-1);
  border-bottom: var(--hairline) solid var(--ink-1);
}

/* Break long URLs / tokens mid-string so a value can't push the page wider
   than the viewport (e.g. the Product Page row's full URL). */
.break-anywhere {
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Per-section source caption: "Source: Product page · checked Jun 2, 2026".
   One quiet line under each content section (spec list, description, key
   features, license) — its links carry the source, the date the freshness. */
.source-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
  color: var(--ink-3);
}

.source-caption__label {
  font-weight: 600;
  color: var(--ink-3);
}

.source-caption__links a {
  color: var(--accent-link);
  text-decoration: none;
  border-bottom: var(--hairline) solid currentColor;
}

.source-caption__date {
  color: var(--ink-3);
}

.source-disclosure {
  margin: var(--space-6) 0;
  padding: var(--space-4) 0;
  border-top: var(--hairline) solid var(--border-subtle);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.source-disclosure summary {
  cursor: pointer;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.source-disclosure ul {
  margin-top: var(--space-3);
  padding-left: 0;
  list-style: none;
}

.source-disclosure li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: baseline;
  color: var(--ink-3);
  font-size: var(--text-meta);
}

.source-disclosure a {
  color: var(--accent-link);
  text-decoration: none;
  border-bottom: var(--hairline) solid currentColor;
}

.source-disclosure__facts,
.source-disclosure__date {
  font-size: 11px;
  color: var(--ink-3);
}

/* ---------- license facts ----------
   Public detail-page policy block. The block is plain, not carded, so it
   reads as reference data beside the compatibility/spec sections. The
   `pricing-*` variants share this styling for the parallel Pricing block
   (e.g. manufacturer-wide education pricing). */
.license-block,
.pricing-block {
  padding: var(--space-6) 0;
  border-top: var(--hairline) solid var(--border-subtle);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.license-block__head,
.pricing-block__head {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.license-facts,
.pricing-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
}

.license-fact,
.pricing-fact {
  min-width: 0;
  padding: var(--space-3) 0;
}

.license-fact dt,
.pricing-fact dt {
  margin-bottom: var(--space-1);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.license-fact dd,
.pricing-fact dd {
  margin: 0;
  color: var(--ink-2);
}

.license-fact__value,
.pricing-fact__value {
  color: var(--ink-1);
  font-weight: 600;
}

.license-fact__meta {
  color: var(--ink-2);
}

/* Manufacturer page: the catalog-wide Licensing and Pricing blocks sit
   side by side (each is only a fact or two) rather than each taking a
   full-width row. The wrapper owns the single top/bottom rule and a
   centered divider; the child blocks drop their own borders. Facts stack
   in one column inside each half so labels never get crushed. Collapses
   to stacked below the mobile breakpoint. */
.mfr-policy-cols {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  border-top: var(--hairline) solid var(--border-subtle);
  border-bottom: var(--hairline) solid var(--border-subtle);
  padding: var(--space-6) 0;
}

.mfr-policy-cols > .license-block,
.mfr-policy-cols > .pricing-block {
  flex: 1 1 240px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.mfr-policy-cols .license-facts,
.mfr-policy-cols .pricing-facts {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 721px) {
  /* Explicit class selectors (not `* + *`) so this out-specifies the
     `border: 0` reset above and the divider actually paints. */
  .mfr-policy-cols > .license-block + .pricing-block,
  .mfr-policy-cols > .pricing-block + .license-block {
    padding-left: var(--space-6);
    border-left: var(--hairline) solid var(--border-subtle);
  }
}

/* ---------- manufacturer reputation ----------
   AI-generated, positives-only "what they're known for" overview built from
   public sources. Editorial, not carded — sits between the plugin count and
   the policy blocks. Sources render as small domain links so any claim is
   checkable; the bullet is the teal square brand mark. */
.mfr-reputation {
  padding: var(--space-6) 0;
  border-top: var(--hairline) solid var(--border-subtle);
}

.mfr-reputation__heading {
  margin: 0 0 var(--space-3);
}

.mfr-reputation__summary {
  margin: 0;
  max-width: 64ch;
  color: var(--ink-1);
  line-height: 1.6;
}

.mfr-reputation__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-1) var(--space-6);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.mfr-reputation__list li {
  position: relative;
  padding-left: var(--space-4);
  color: var(--ink-2);
  line-height: 1.5;
}

.mfr-reputation__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 5px;
  height: 5px;
  background: var(--accent);
}

.mfr-reputation__meta {
  margin: var(--space-4) 0 0;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  color: var(--ink-3);
}

.mfr-reputation__meta a {
  color: var(--ink-3);
  text-decoration: underline;
}

.mfr-reputation__meta a:hover {
  color: var(--accent-link);
}

@media (max-width: 720px) {
  .mfr-reputation__list {
    grid-template-columns: minmax(0, 1fr);
  }
}


/* Used for section dividers under the hero. */
.section-title {
  margin-top: var(--space-10);
}

/* Lede paragraph — slightly larger than body. Used at the top of
   plugin detail and category pages. */
.lede {
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 64ch;
}

/* ---------- a11y ---------- */
:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

/* Skip layout animations under reduced-motion (already zeroed in tokens). */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0ms !important;
    animation-duration: 0ms !important;
  }
}

/* Source: web/static/css/components/gui-tile.css */
/* ---------- GUI tile ----------
   One standardized treatment for every plugin image: a hard square with the
   GUI contained at ~82% over a blurred, brightened copy of itself, lifted off
   the backdrop with a drop shadow. Squares up wildly different GUI aspect
   ratios (ultra-wide racks, tall channel strips, square synths) so they never
   break a grid or column. Size + blur are tuned per context via the --gt-*
   custom properties and the modifier classes below. */
.gui-tile {
  --gt-blur: 14px;
  --gt-shadow: drop-shadow(0 6px 16px rgba(0, 0, 0, 0.45))
               drop-shadow(0 2px 5px rgba(0, 0, 0, 0.35));
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-subtle);
}

/* Background fill: the plugin's own image, blown up, brightened and blurred. */
.gui-tile__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(var(--gt-blur)) brightness(1.4);
  transform: scale(1.9);   /* scale past the edges so the blur never reveals them */
}

/* Foreground: the sharp GUI, contained and centred, lifted with a shadow. */
.gui-tile__fg {
  position: absolute;
  inset: 0;
  margin: auto;
  width: auto;
  height: auto;
  max-width: 82%;
  max-height: 82%;
  object-fit: contain;
  filter: var(--gt-shadow);
}

/* Missing-image placeholder: the bare square, no layers. */
.gui-tile--empty {
  background-color: var(--surface-card-nested);
}

/* ---- size / context modifiers ---- */

/* Small list + table rows (replaces the old 64px .thumb). */
.gui-tile--thumb {
  width: 64px;
  --gt-blur: 7px;
  --gt-shadow: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
}

/* Similar-plugins rail. */
.gui-tile--rail {
  --gt-blur: 11px;
  margin-bottom: var(--space-2);
}

/* Compare-table column header. */
.gui-tile--compare {
  max-width: 220px;
}

/* Plugin-detail lead image. Fixed square on desktop; responsive.css makes it
   full-width once the header stacks. */
.gui-tile--hero {
  width: 360px;
  max-width: 100%;
  align-self: start;
  --gt-blur: 16px;
}

/* Source: web/static/css/components/plugin-detail.css */
/* ---------- identity / hero ---------- */
.identity {
  display: grid;
  /* accent | photo | text+description | features+resources. The product
     photo leads on the left (store-page convention); the two text columns
     shift one to the right. responsive.css collapses to a single stacked
     column below 1024px. */
  grid-template-columns: 4px auto minmax(0, 1.5fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

.identity__accent {
  align-self: stretch;
  /* Span every row so the stripe runs the full header height, including
     the hero row once it stacks under the text on mobile. */
  grid-row: 1 / -1;
  background-color: var(--accent);
  /* Falls back to a min so the bar shows on short headers too. */
  min-height: 48px;
}

.identity__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.identity__manufacturer {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.identity__tagline {
  font-size: var(--text-body);
  line-height: var(--text-body-line);
  color: var(--ink-2);
  font-style: italic;
}

/* Spec list — the verifiable facts as aligned label → value → ⓘ rows.
   Shared by the hero "at a glance" block and the License section so both read
   identically. The marker is pushed to the right edge of the value column, so
   every ⓘ lines up in one consistent column regardless of value length. */
.spec-list {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
}

.spec-row {
  display: grid;
  grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--space-5);
}

.spec-row__label {
  margin: 0;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.spec-row__value {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  min-width: 0;
  color: var(--ink-1);
}

.spec-row__meta {
  color: var(--ink-3);
  font-size: var(--text-meta);
}

/* Push the marker to the right edge of the value column → consistent ⓘ column. */
.spec-row__mark {
  margin-left: auto;
  align-self: center;
}
.spec-row__mark:empty {
  display: none;
}

/* The plugin-detail lead image uses the shared .gui-tile component
   (components/gui-tile.css), sized via the .gui-tile--hero modifier and
   placed full-width once the header stacks (responsive.css). */

/* The description, moved up under the format/license pills in column 1. */
.identity__description {
  margin-top: var(--space-2);
}

/* Column 2 of the hero: key features + external resources, stacked. */
.identity__features {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  min-width: 0;
}

/* Quiet eyebrow heading inside the hero columns ("Key features",
   "External resources") — subordinate to the H1, matches the
   .identity__manufacturer / .buy-promote__label treatment. */
.identity__subhead {
  margin: 0 0 var(--space-3);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.identity__features-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.identity__features-list li {
  position: relative;
  padding-left: var(--space-4);
  color: var(--ink-2);
  line-height: 1.5;
}

.identity__features-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 5px;
  height: 5px;
  background: var(--accent);
}

/* ---------- action panel (plugin detail) ----------
   Holds every interactive control for the plugin — the "View at
   retailer" CTA, trial-download links, Favorite / Track price /
   Recommend / Flag — in one row between the hero image and
   the pricing card. Splitting the panel out of buy-promote separates
   "what can I do with this plugin" (here) from "what does it cost"
   (below), so a visitor scans top-down and the price block isn't
   competing with five buttons for the same eye-row.

   Visually it reads as a sibling to buy-promote and the identity card
   above: same card surface, same hairline, same accent left-border so
   the three stacked sections share a single vertical accent stripe. */
.action-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-left: 4px solid var(--accent);
}

.action-panel__buttons {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* ---------- buy promote (plugin detail) ----------
   Used to be the price + actions card; the actions moved into
   .action-panel above, so this is now just the price headline and the
   optional Where-to-buy breakdown. The accent left-border stays — it's
   what marks this as the primary pricing surface on the page. */
.buy-promote {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  padding: var(--space-6) var(--space-8);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-left: 4px solid var(--accent);
}

/* Price headline + the primary "View At" buy CTA share one row — price on
   the left, CTA in the open top-right space of the card. Wraps when narrow. */
.buy-promote__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-5);
}

.buy-promote__price {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.buy-promote__label {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.buy-promote__amount {
  font-size: 22px;
  line-height: 1.2;
  color: var(--ink-1);
  font-weight: 600;
}

/* "was $X · −Y%" under the lowest-price headline when it's a sale.
   Reuses the catalog row's .price-was / .price-off colors. */
.buy-promote__sale {
  font-size: var(--text-ui);
  line-height: 1.3;
}

.buy-promote__source {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

.buy-promote__context {
  max-width: 44rem;
  font-size: var(--text-ui);
  line-height: 1.45;
  color: var(--ink-2);
}

.buy-promote__missing {
  color: var(--ink-2);
  font-size: var(--text-body);
  line-height: var(--text-body-line);
}

/* Education pricing — a pricing fact, shown here in the buy card rather
   than in the License block. Quiet label + emphasized value on one line. */
.buy-promote__edu {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
}

.buy-promote__edu-label {
  color: var(--ink-3);
}

.buy-promote__edu-value {
  color: var(--ink-1);
  font-weight: 500;
}

.buy-promote__stamp {
  /* Same color/size as the source line so the freshness signal sits
     calmly next to "at <retailer>" rather than competing with the
     price. The dot separator (rendered in the template) keeps the two
     parts visually attached. */
  color: var(--ink-3);
}

.price-stamp {
  /* Mono font is inherited from td.num; suppress so the timestamp
     reads as ordinary text and doesn't crowd the price line. */
  font-family: var(--font-sans);
  margin-top: var(--space-1);
  font-variant-numeric: normal;
}

.buy-promote__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-4) var(--space-6);
  background-color: var(--accent);
  color: var(--surface-card);
  border: var(--hairline) solid var(--accent);
  border-bottom: var(--hairline) solid var(--accent);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-align: center;
  text-decoration: none;
  transition: background-color var(--transition-default),
    border-color var(--transition-default);
}

.buy-promote__cta:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--surface-card);
  /* Override the global a:hover underline so the CTA stays a button. */
  border-bottom-color: var(--accent-hover);
}

[data-theme="dark"] .buy-promote__cta {
  color: var(--surface-page);
}

[data-theme="dark"] .buy-promote__cta:hover {
  color: var(--surface-page);
}

/* Trial/demo download buttons: secondary to the buy CTA — outlined in
   the accent rather than filled, so "View at retailer" stays primary. */
.buy-promote__cta--trial,
.buy-promote__cta--trial:hover {
  background-color: var(--surface-card);
  color: var(--accent);
  border-color: var(--accent);
}

.buy-promote__cta--trial:hover {
  background-color: var(--accent);
  color: var(--surface-card);
}

[data-theme="dark"] .buy-promote__cta--trial {
  color: var(--accent);
}

[data-theme="dark"] .buy-promote__cta--trial:hover {
  color: var(--surface-page);
}

.buy-promote__share {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  background-color: var(--surface-card);
  color: var(--ink-2);
  border: var(--hairline) solid var(--border-strong);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  cursor: pointer;
  transition: background-color var(--transition-default),
    border-color var(--transition-default),
    color var(--transition-default);
}

.buy-promote__share:hover {
  background-color: var(--surface-rail);
  border-color: var(--ink-3);
  color: var(--ink-1);
}

.buy-promote__share:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.buy-promote__track {
  background-color: var(--surface-card);
  /* Match the other action buttons: icon gap + Title Case label (override
     the uppercase + zero-gap from the shared .watchlist-card__toggle base). */
  gap: var(--space-2);
  text-transform: none;
  letter-spacing: normal;
}

.buy-promote__watch-status {
  flex: 1 1 100%;
  color: var(--ink-3);
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
}

/* Logged-out value-prop under the action buttons: explains what
   "Track price" does (and that an account is free) so the watchlist
   sells itself before the click opens the auth gate. Hidden for
   signed-in visitors (the template gates it on current_user). */
.action-panel__pitch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  font-size: var(--text-ui);
  line-height: 1.45;
  color: var(--ink-3);
}

.action-panel__pitch svg {
  flex: 0 0 auto;
  color: var(--accent);
}

.buy-promote__watch-form {
  padding: var(--space-5);
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-subtle);
}

.buy-promote__offers {
  padding-top: var(--space-5);
  border-top: var(--hairline) solid var(--border-subtle);
}

.buy-promote__offers-title {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.buy-promote__offers .where-to-buy {
  margin-top: var(--space-3);
}

/* Deal-quality badge under the headline price — historical context
   ("Lowest price we've tracked · since May 2026"). */
.buy-promote__deal-quality {
  margin-top: var(--space-2);
}

/* Per-retailer "was → −%" line in the where-to-buy table, mirroring the
   headline price card's sale treatment. */
.where-to-buy__sale {
  font-size: var(--text-meta);
  line-height: 1.3;
}

.where-to-buy__spark-cell {
  width: 140px;
  vertical-align: middle;
}

.price-spark {
  display: block;
  width: 100%;
  height: 28px;
  max-width: 140px;
  overflow: visible;
  cursor: crosshair;
}

/* Pooled price-over-time chart above the where-to-buy table. The SVG
   reuses .price-spark for the hover-tooltip wiring; these overrides must
   come after .price-spark to beat its sparkline-cell sizing. */
.price-history {
  margin-bottom: var(--space-6);
}

.price-history__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
}

.price-history__meta {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

.price-history__chart {
  height: 120px;
  max-width: none;
  margin-top: var(--space-3);
}

.spark-tooltip {
  position: fixed;
  pointer-events: none;
  background: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-ui);
  line-height: 1.2;
  border-radius: 4px;
  z-index: 1000;
  white-space: nowrap;
  transform: translate(-50%, calc(-100% - 8px));
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

.spark-tooltip[hidden] {
  display: none;
}

.where-to-buy__retired td {
  color: var(--ink-3);
}

/* External resources — official off-site links (product page, manual,
   support) for a plugin. A quiet column of labelled links, each trailed
   by the shared external-link glyph. */
.external-resources {
  margin: var(--space-10) 0 0;
}

.external-resources__list {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.external-resources__list a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Shared external-link glyph: never shrink, sits inline with its label. */
.external-icon {
  flex: 0 0 auto;
}

/* ---------- DAW compatibility (plugin detail) ----------
   Sits between the pricing card and the license block, sharing their card
   surface + accent left-border so the stacked sections read as one accented
   column. A row of per-DAW badges: only "supported" is a filled teal disc;
   "unconfirmed" / "unsupported" are teal-outlined (their glyph tells them
   apart), keeping the chart on-brand instead of traffic-light red/amber. */
.daw-compat {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-6) var(--space-8);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-left: 4px solid var(--accent);
}

.daw-compat__head h2 {
  margin: 0;
  font-size: var(--text-h2);
  line-height: var(--text-h2-line);
  font-weight: var(--text-h2-weight);
}

.daw-compat__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: var(--space-5) var(--space-3);
}

.daw-compat__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
  min-width: 0;
}

.daw-compat__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
}

.daw-compat__icon svg {
  width: 18px;
  height: 18px;
}

/* Only "supported" is filled — solid teal disc, white glyph (the teal
   fill is constant across themes, so white reads on both). */
.daw-compat__icon--yes {
  background-color: var(--accent);
  color: #fff;
}

/* "unconfirmed" + "unsupported": teal ring, no fill. Glyph uses --ink-1
   so it's cream-on-dark (matching the mock) and dark-on-light. */
.daw-compat__icon--unknown,
.daw-compat__icon--no {
  background-color: transparent;
  border: 2px solid var(--accent);
  color: var(--ink-1);
}

.daw-compat__q {
  font-weight: 700;
  font-size: 17px;
  line-height: 1;
}

.daw-compat__name {
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
  color: var(--ink-2);
}

.daw-compat__note {
  margin: 0;
  font-size: var(--text-meta);
  line-height: var(--text-meta-line);
  color: var(--ink-3);
}

.daw-compat__legend {
  white-space: nowrap;
  color: var(--ink-4);
}

/* Eight badges get cramped on phones — wrap to two rows of four. */
@media (max-width: 560px) {
  .daw-compat__grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* ---------- tag section (plugin detail) ----------
   Descriptive tag groups (capabilities, character, interface, use cases)
   gathered into one teal-accented block, the groups laid out in two columns
   so they read as a compact unit instead of a tall stack of full-width
   headings. The left accent bar matches the identity / pricing surfaces. */
.tag-section {
  margin-top: var(--space-10);
  padding: var(--space-5) 0 var(--space-5) var(--space-6);
  border-left: 4px solid var(--accent);
}

.tag-section__groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5) var(--space-8);
}

.tag-section__group {
  min-width: 0;
}

.tag-section__label {
  margin: 0 0 var(--space-3);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ---------- collapsible tag rows (desktop) ----------
   A group with many chips can wrap into a tall stack. On desktop tag-collapse.js
   measures each row and, when it runs past one line, clamps it to --tag-collapsed-h
   and reveals the toggle below. The clamp is scoped to >720px so it never fights the
   mobile horizontal-swipe rows in responsive.css. */
@media (min-width: 721px) {
  .tag-section .tag-row.is-collapsed {
    overflow: hidden;
    max-height: var(--tag-collapsed-h, none);
  }
}

.tag-expand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-link);
}

.tag-expand[hidden] {
  display: none;
}

.tag-expand:hover {
  text-decoration: underline;
}

.tag-expand__chevron {
  transition: transform var(--transition-default);
}

.tag-expand[aria-expanded="true"] .tag-expand__chevron {
  transform: rotate(180deg);
}

/* Source: web/static/css/components/bundles.css */
/* ---------- bundle grid (index) ---------- */
.bundle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-6);
}

.bundle-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-6);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-left: 4px solid var(--accent);
  color: var(--ink-1);
  /* The card itself is the click target. Override the global a:hover
     underline so the card stays a card. */
  border-bottom: var(--hairline) solid var(--border-strong);
  transition: border-color var(--transition-default),
    background-color var(--transition-default);
}

.bundle-card:hover {
  background-color: var(--surface-card-nested);
  border-color: var(--border-strong);
  border-left-color: var(--accent-hover);
  color: var(--ink-1);
  border-bottom-color: var(--border-strong);
}

.bundle-card__media {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 10;
  margin: calc(var(--space-6) * -1) calc(var(--space-6) * -1) var(--space-2);
  padding: var(--space-4);
  background: var(--surface-rail);
  border-bottom: var(--hairline) solid var(--border-subtle);
  overflow: hidden;
}

.bundle-card__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.bundle-card__eyebrow {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.bundle-card__name {
  font-size: var(--text-h2);
  line-height: var(--text-h2-line);
  font-weight: 600;
  color: var(--ink-1);
}

.bundle-card__desc {
  color: var(--ink-2);
  /* Two-line clamp so a long description doesn't blow up card height
     and break the grid rhythm. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.bundle-card__price-row {
  margin-top: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--hairline) solid var(--border-subtle);
}

.bundle-card__price {
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--ink-1);
}

.bundle-card__savings {
  font-size: var(--text-meta);
  color: var(--accent-link);
}

.identity--bundle {
  grid-template-columns: 4px minmax(0, 1fr);
}

.identity--bundle-with-image {
  grid-template-columns: 4px minmax(220px, 280px) minmax(0, 1fr);
}

.bundle-detail__image {
  width: min(280px, 100%);
  aspect-ratio: 16 / 10;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--surface-rail);
  border: var(--hairline) solid var(--border-subtle);
  overflow: hidden;
}

.bundle-detail__image img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ---------- savings callout (bundle detail) ---------- */
.savings-callout {
  padding: var(--space-5) var(--space-8);
  background-color: color-mix(in srgb, var(--accent) 8%, var(--surface-card));
  border: var(--hairline) solid color-mix(in srgb, var(--accent) 25%, transparent);
  border-left: 4px solid var(--accent);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.savings-callout__headline {
  font-size: var(--text-h2);
  line-height: var(--text-h2-line);
  font-weight: 600;
  color: var(--ink-1);
}

.savings-callout__pct {
  color: var(--accent-link);
  font-weight: 500;
}

.savings-callout__detail {
  color: var(--ink-2);
  font-size: var(--text-body);
}

/* ---------- bundle callout (plugin detail) ----------
   Sits high on the plugin page so a shopper sees "this is in N
   bundles, save $X" before scrolling into description/specs. Square,
   quiet — the savings stand out, not the chrome. */
.bundle-callout {
  padding: var(--space-5) var(--space-6);
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-subtle);
}

.bundle-callout__head {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
  margin-bottom: var(--space-3);
}

.bundle-callout__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.bundle-callout__list li + li {
  margin-top: 0;
  padding-top: var(--space-2);
  border-top: var(--hairline) solid var(--border-subtle);
}

.bundle-callout__link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
  color: var(--ink-1);
  border-bottom: none;
  padding: var(--space-2) 0;
}

.bundle-callout__link:hover {
  color: var(--accent-link);
  border-bottom: none;
}

.bundle-callout__name {
  font-weight: 600;
}

.bundle-callout__meta {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-meta);
  color: var(--ink-3);
}

.bundle-callout__save {
  color: var(--accent-link);
}

/* Source: web/static/css/components/related-rail.css */
/* ---------- related rail ----------
   Used on one-plugin manufacturer pages so the page keeps a browse path
   into the plugin's primary category instead of ending after one row. */
.related-rail {
  padding-top: var(--space-8);
  border-top: var(--hairline) solid var(--border-subtle);
}

.related-rail__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-5);
  margin-bottom: var(--space-4);
}

.related-rail__head h2 {
  font-size: var(--text-h2);
  line-height: var(--text-h2-line);
}

.related-rail__head a {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.related-rail__pivots,
.related-rail__fallback {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: calc(var(--space-2) * -1) 0 var(--space-4);
  font-size: var(--text-meta);
  line-height: 1.35;
  color: var(--ink-3);
}

.related-rail__pivots > span:first-child {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.related-rail__pivots a,
.related-rail__fallback {
  color: var(--accent-link);
  text-decoration: none;
  border-bottom: var(--hairline) solid currentColor;
}

.related-rail__items {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--space-3);
}

.related-rail__item {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-height: 72px;
  padding: var(--space-4);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  border-bottom: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
}

.related-rail__item:hover {
  background-color: var(--surface-card-nested);
  border-color: var(--border-strong);
  border-bottom-color: var(--border-strong);
  color: var(--ink-1);
}

.related-rail__item--plugin {
  min-height: 160px;
}

/* Rail thumbnail now uses the shared .gui-tile component, sized by the
   .gui-tile--rail modifier (components/gui-tile.css). */

.related-rail__name {
  font-weight: 600;
}

.related-rail__meta {
  color: var(--ink-3);
  font-size: var(--text-meta);
}

.related-rail__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-height: 52px;
  margin-top: var(--space-2);
}

.related-rail__tag {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-height: 24px;
  padding: var(--space-1) var(--space-2);
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-subtle);
  color: var(--ink-2);
  font-size: var(--text-meta);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  overflow-wrap: anywhere;
}

/* Source: web/static/css/components/forms.css */
/* ---------- flag form ----------
   Lives below the buy-promote action row on plugin detail. The
   collapse pattern matches the watchlist editor for muscle memory. */
.buy-promote__flag-form {
  margin-top: var(--space-5);
  padding: var(--space-5);
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-subtle);
}

.buy-promote__flag-form label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-ui);
  color: var(--ink-2);
}

.buy-promote__flag-form textarea {
  width: 100%;
  font-family: inherit;
  padding: var(--space-3);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  resize: vertical;
  box-sizing: border-box;
}

.buy-promote__flag-actions {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

.buy-promote__flag-status {
  margin-top: var(--space-3);
  color: var(--ink-3);
  font-size: var(--text-meta);
}

[data-flag-toggle][data-already-flagged="true"] {
  opacity: 0.65;
  cursor: default;
}

/* Source: web/static/css/components/feedback.css */
/* ---------- recommend (thumbs-up) ----------
   Recommend button reuses the .buy-promote__favorite shell so the
   plugin-page button row stays visually consistent. The thumbs-up
   icon, accent color when active, and the count chip distinguish it
   from the heart. */
.thumbs-up {
  display: inline-block;
  vertical-align: middle;
  color: currentColor;
}

.buy-promote__recommend {
  gap: var(--space-2);
}

.buy-promote__recommend.is-recommended,
.manufacturer-head__recommend.is-recommended {
  border-color: var(--accent);
  background-color: color-mix(in srgb, var(--accent) 8%, transparent);
  color: var(--accent-link);
}

.manufacturer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  margin-bottom: var(--space-3);
  flex-wrap: wrap;
}

.manufacturer-head h1 { margin: 0; }

/* Logo + name group sits left; the recommend button stays right (and wraps
   below on narrow screens via the parent's flex-wrap). */
.manufacturer-head__identity {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  min-width: 0;
}

/* Hand-uploaded company logos, auto-tinted to the theme. The logo image is
   used as a CSS mask and painted in the current ink color, so only its
   silhouette matters — it reads dark-on-light / light-on-dark and re-tints
   when the theme flips, with no backing chip. The uploaded file's own colors
   are irrelevant (any color or a multi-color mark collapses to one tint).
   The hidden inner <img> reserves the logo's intrinsic box so the mask sizes
   correctly without us storing an aspect ratio; loaded as a mask image, the
   SVG is never inlined/executed (no XSS surface). */
.mfr-logo {
  display: inline-block;
  height: 48px;
  background-color: var(--ink-1);
  -webkit-mask-image: var(--logo-src);
          mask-image: var(--logo-src);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: left center;
          mask-position: left center;
  -webkit-mask-size: contain;
          mask-size: contain;
  flex: 0 0 auto;
}

.mfr-logo > img {
  display: block;
  height: 100%;
  width: auto;
  visibility: hidden;  /* reserves the box; the masked background is the logo */
}

/* Manufacturers list: the logo sits in a shared-width "cell" so every name
   starts at the same x, with a hairline divider between the logo cell and the
   name. The header row carries no logo, so the "Name" heading doesn't shift.
   measure-logo-cell.js sizes --mfr-logo-cell to the widest logo's rendered
   width so the column hugs the logos; until it runs (or with no logos) each
   logo keeps its own intrinsic box. */
.mfr-list-name {
  --mfr-logo-cell: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
}

/* Holds the logo (or nothing, for logo-less rows), reserves the shared cell
   width, and centers each logo within it. */
.mfr-logo--thumb {
  height: 26px;
  width: var(--mfr-logo-cell);
  flex: 0 0 var(--mfr-logo-cell);
  -webkit-mask-position: center;
          mask-position: center;
}

.mfr-list-name > strong {
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  padding-left: var(--space-3);
  border-left: var(--hairline) solid var(--border-subtle);
}

.recommend-count {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* /recommended page columns. */
.col-rank { width: 3rem; color: var(--ink-3); }
.col-recommends { width: 8rem; }

/* Catalog-row recommend + favorite share the trailing actions cell. */
.row-actions {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

.row-recommend {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  height: 28px;
  padding: 0 var(--space-2);
  background: transparent;
  border: var(--hairline) solid transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: var(--text-ui);
  line-height: 1;
  cursor: pointer;
  transition: color var(--transition-default),
    border-color var(--transition-default);
}

.row-recommend:hover { color: var(--ink-1); }

.row-recommend:focus-visible {
  outline: none;
  border-color: var(--ink-3);
  box-shadow: var(--focus-ring);
}

.row-recommend.is-recommended { color: var(--accent-link); }

.row-recommend__count { font-variant-numeric: tabular-nums; }

/* Watchlist (price-tracking) toggle in the Actions cell. Same 28px
   icon-button shell as the favorite heart so the three controls read as
   one consistent set; a bell — not a bookmark — so it stays distinct
   from the heart. Accent color when the plugin is being tracked. */
.row-watchlist {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: var(--hairline) solid transparent;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--transition-default),
    border-color var(--transition-default);
}

.row-watchlist:hover { color: var(--ink-1); }

.row-watchlist:focus-visible {
  outline: none;
  border-color: var(--ink-3);
  box-shadow: var(--focus-ring);
}

.row-watchlist.is-watching { color: var(--accent-link); }

/* ---------- flag banner ----------
   Public warning that appears on a plugin's page once it has been
   reported 3+ times. Pink-on-pink so it reads as caution without
   the alarm of a red error chrome. */
.flag-banner {
  margin: 0 0 var(--space-6);
  padding: var(--space-4) var(--space-5);
  background-color: #fde0ec;
  border: var(--hairline) solid #f1a8c5;
  color: #6e2547;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
}

.flag-banner strong { color: #5a1d3a; }

[data-theme="dark"] .flag-banner {
  background-color: color-mix(in srgb, #e91e63 18%, transparent);
  border-color: color-mix(in srgb, #e91e63 40%, transparent);
  color: #f8c4d7;
}

[data-theme="dark"] .flag-banner strong { color: #ffe2ed; }

/* ---------- affiliate note ----------
   Shown next to "View at X" retailer CTAs on plugin/bundle detail and
   compare pages. Per FTC guidance the disclosure must be near the
   affiliate link, at least as large as the surrounding text, and not
   hidden in light-gray small print — so this uses body color and
   normal weight, not the muted caption styling we use elsewhere. */
.affiliate-note {
  margin: var(--space-2) 0 0;
  padding: 0;
  color: var(--ink-2);
  font-size: var(--text-body);
  line-height: var(--text-body-line);
}
/* When the note lives inside a flex row of action buttons, force it
   onto its own full-width row directly under the CTA it discloses,
   instead of flowing inline between the buttons. */
.action-panel__buttons .affiliate-note,
.buy-promote .affiliate-note {
  flex-basis: 100%;
}
.affiliate-note a {
  color: var(--accent);
}
/* When the note sits above the retailer table, give it a hair more
   breathing room so it doesn't look glued to the table border. */
.affiliate-note--table {
  margin-bottom: var(--space-3);
}

/* ---------- success flash ----------
   Site-wide confirmation banner shown after a positive action (e.g.
   landing back on the homepage just after verifying an email).
   Accent-on-accent so it reads as on-brand acknowledgement rather than
   the caution chrome of .flag-banner. The ?verified flag is stripped
   from the URL via JS in base, so a refresh dismisses it. */
.flash-success {
  margin: 0 0 var(--space-6);
  padding: var(--space-4) var(--space-5);
  background-color: color-mix(in srgb, var(--accent) 12%, transparent);
  border: var(--hairline) solid color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--ink-1);
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
}

/* Source: web/static/css/pages/home.css */
/* ---------- hero (home page) ---------- */
.hero {
  padding: var(--space-6) 0 var(--space-8);
  border-bottom: var(--hairline) solid var(--border-subtle);
  max-width: 720px;
}

.hero h1 {
  font-size: 32px;
  line-height: 1.2;
  letter-spacing: -0.015em;
}

.hero__lede {
  margin-top: var(--space-5);
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 60ch;
}

/* ---------- "On sale now" deal rail (home page) ----------
   Sits between the hero and the catalog. Surfaces the biggest current
   price drops so the home reads as a live, worth-bookmarking deal index
   rather than a static table. Hidden by the route once filters are on. */
.deal-rail {
  margin: var(--space-10) 0;
}

.deal-rail__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}

.deal-rail__title {
  font-size: 18px;
  line-height: 1.2;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.deal-rail__all {
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
  white-space: nowrap;
}

/* Logged-out pitch tying the deal rail (desire) to an account (capture).
   Sits between the rail head and the grid; gated on current_user in the
   template so signed-in visitors never see it. */
.deal-rail__pitch {
  margin: 0 0 var(--space-5);
  font-size: var(--text-ui);
  line-height: 1.5;
  color: var(--ink-3);
}

.deal-rail__pitch-cta {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-link);
  cursor: pointer;
}

.deal-rail__pitch-cta:hover {
  text-decoration: underline;
}

.deal-rail__pitch-cta:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.deal-rail__grid {
  /* One row, no scrollbar. The card count is capped server-side (see
     `_deal_rail_context`) to a teaser handful, so the cards scale to fill a
     single line — no wrapping to a second row, no horizontal scroll. Left-
     aligned (default): a thin set of deals stays left under the heading
     rather than centering. The mobile rule below switches to a swipe rail
     for the narrow-screen peek. */
  display: flex;
  gap: var(--space-4);
}

.deal-card {
  position: relative;
  /* One-row rail item, hybrid sizing: cards grow equally to fill the row
     (scaling up/down with the viewport) but never past `max-width`, so the
     capped handful of deals always spans one line with no scrollbar — and a
     thin set (e.g. just two on sale) scales up to a sensible size instead of
     ballooning across the whole width. `min-width: 0` lets them shrink to fit
     narrow desktops rather than overflow. */
  flex: 1 1 0;
  min-width: 0;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-subtle);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--transition-default);
}

.deal-card:hover {
  border-color: var(--accent);
}

/* Deal-card thumbnail now uses the shared .gui-tile component
   (components/gui-tile.css) — a square GUI tile. The card's flex `gap`
   handles the spacing the old thumb's margin used to. */

/* Discount flag, top-right over the thumbnail. */
.deal-card__off {
  position: absolute;
  top: var(--space-5);
  right: var(--space-5);
  padding: 1px var(--space-2);
  background-color: var(--accent);
  color: #15110d;
  font-size: var(--text-meta);
  font-weight: 600;
}

.deal-card__name {
  font-size: var(--text-body);
  line-height: 1.3;
  font-weight: 600;
  color: var(--ink-1);
}

.deal-card__mfr {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

.deal-card__price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.deal-card__now {
  font-size: var(--text-h2);
  font-weight: 600;
  color: var(--ink-1);
}

/* Deal-quality note ("Lowest tracked", "3-month low") under the price. */
.deal-card__quality {
  margin-top: var(--space-1);
  font-size: var(--text-meta);
  font-weight: 600;
  color: var(--accent-link);
}

@media (max-width: 720px) {
  /* Horizontal-scroll rail on phones so the deals stay one row tall and
     don't push the catalog far down. Cards sized to ~72% so the next one
     peeks, signalling the row swipes. */
  .deal-rail__grid {
    overflow-x: auto;
    scroll-snap-type: x proximity;
    padding-bottom: var(--space-3);
    -webkit-overflow-scrolling: touch;
  }

  .deal-card {
    flex: 0 0 72%;
    max-width: none;
    scroll-snap-align: start;
  }
}

/* Source: web/static/css/pages/compare.css */
/* /compare table — one column per plugin, one row per field. Cells
   that disagree across columns get a soft accent background so the
   user can scan for differences without reading every value. */
.compare-table-wrapper {
  overflow-x: auto;
}

.compare-table {
  width: 100%;
  min-width: 600px;
  border-collapse: collapse;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  /* Fixed layout so the plugin columns share the remaining width evenly
     instead of sizing to their content. The label column is pinned; the
     rest split equally regardless of how many plugins are compared. */
  table-layout: fixed;
}

.compare-table thead th:first-child {
  width: 160px;
}

.compare-table th,
.compare-table td {
  padding: var(--space-4) var(--space-6);
  border-bottom: var(--hairline) solid var(--border-subtle);
  border-right: var(--hairline) solid var(--border-subtle);
  vertical-align: top;
  text-align: left;
  overflow-wrap: break-word;
}

.compare-table th:last-child,
.compare-table td:last-child {
  border-right: none;
}

.compare-table thead th {
  background-color: var(--surface-rail);
  color: var(--ink-1);
  /* Image heights vary per plugin; anchor the hero + name + vendor to the
     bottom so the names sit on a common baseline above the body rows
     instead of floating at the top of the tallest cell. */
  vertical-align: bottom;
}

.compare-table__hero-link {
  display: block;
  margin-bottom: var(--space-3);
  border-bottom: none;
}

/* The compare column header image now uses the shared .gui-tile component,
   sized by the .gui-tile--compare modifier (components/gui-tile.css). A
   square tile keeps portrait and landscape product shots one height. */

.compare-table tbody th {
  width: 160px;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
}

.compare-table__cell--differs {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Price cell: the amount with a "View at <retailer>" link to the offer
   behind that lowest price. Wraps under the amount on narrow columns. */
.compare-price {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
}

.compare-price__amount {
  color: var(--ink-1);
}

.compare-price__buy {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-family: var(--font-sans);
  font-size: var(--text-meta);
  color: var(--accent-link);
  white-space: nowrap;
  border-bottom: none;
}

.compare-price__buy:hover {
  color: var(--accent);
}

.compare-price__buy .external-icon {
  flex: 0 0 auto;
}

/* Key-features list: base.css indents <ul> by --space-8, which pushes the
   bullets in from the column's left edge. Pull them back so they line up
   with the other rows' values, keeping a hanging indent on wrapped lines. */
.compare-table td ul {
  margin: 0;
  padding-left: 0;
  list-style: none;
}

.compare-table td li {
  position: relative;
  padding-left: 1.1em;
}

.compare-table td li::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--ink-3);
}

/* Source: web/static/css/pages/favorites.css */
/* ---------- favorites ----------
   DB-backed via the anonymous session cookie; share the watchlist
   button look in the header, but with a heart icon and acting as a
   link to /favorites rather than a panel toggle. */
.favorites-toggle {
  text-decoration: none;
  gap: var(--space-2);
}

.favorites-toggle:hover {
  text-decoration: none;
}

.fav-heart {
  display: inline-block;
  vertical-align: middle;
  color: currentColor;
}

.fav-heart--filled { display: none; }

[data-favorite-toggle].is-favorited .fav-heart--outline,
.favorites-list__remove .fav-heart--outline {
  display: none;
}

[data-favorite-toggle].is-favorited .fav-heart--filled,
.favorites-list__remove .fav-heart--filled {
  display: inline-block;
}

[data-favorite-toggle].is-favorited {
  color: var(--accent-link);
}

.buy-promote__favorite {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  background-color: var(--surface-card);
  color: var(--ink-2);
  border: var(--hairline) solid var(--border-strong);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  cursor: pointer;
  transition: background-color var(--transition-default),
    border-color var(--transition-default),
    color var(--transition-default);
}

.buy-promote__favorite:hover {
  background-color: var(--surface-rail);
  border-color: var(--ink-3);
  color: var(--ink-1);
}

.buy-promote__favorite:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.buy-promote__favorite.is-favorited {
  border-color: var(--accent);
  background-color: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* Per-row heart in the catalog table. Width-bounded so the column
   doesn't stretch on viewports where the price column is short. */
.col-fav { width: 5.5rem; text-align: right; }

.row-favorite {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: var(--hairline) solid transparent;
  color: var(--ink-3);
  cursor: pointer;
  transition: color var(--transition-default),
    border-color var(--transition-default);
}

.row-favorite:hover {
  color: var(--ink-1);
}

.row-favorite:focus-visible {
  outline: none;
  border-color: var(--ink-3);
  box-shadow: var(--focus-ring);
}

.row-favorite.is-favorited {
  color: var(--accent-link);
}

/* Standalone /favorites page. */
.col-fav-action { width: 7rem; text-align: right; }

.favorites-list__remove {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  background-color: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent-link);
  border: var(--hairline) solid color-mix(in srgb, var(--accent) 30%, transparent);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  cursor: pointer;
}

.favorites-list__remove:hover {
  background-color: color-mix(in srgb, var(--accent) 18%, transparent);
}

.favorites-list__remove:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
}

.favorites-empty {
  margin-top: var(--space-8);
  padding: var(--space-8);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-subtle);
  color: var(--ink-3);
}

.favorites-empty p { margin: 0 0 var(--space-4); }
.favorites-empty p:last-child { margin: 0; }

.hero--compact { padding-bottom: var(--space-4); }

/* Source: web/static/css/pages/contact.css */
/* ---------- contact form ---------- */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 36rem;
  margin-top: var(--space-6);
}

.contact-form__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.contact-form__field > span {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.contact-form__field input,
.contact-form__field select,
.contact-form__field textarea {
  font-family: inherit;
  font-size: 1rem;
  padding: var(--space-3);
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
}

.contact-form__field textarea {
  resize: vertical;
  min-height: 8rem;
}

.contact-form__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.contact-form__submit {
  align-self: flex-start;
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
}

.contact-form__submit:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

/* Source: web/static/css/pages/auth.css */
/* ---------- auth (sign in / create account) ---------- */
.auth {
  display: flex;
  justify-content: center;
  padding: var(--space-8) var(--space-4);
}

.auth__card {
  width: 100%;
  max-width: 24rem;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-subtle);
  padding: var(--space-6);
}

.auth__title {
  margin: 0 0 var(--space-5);
}

.auth__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.auth__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.auth__field > span {
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.auth__field input {
  font-family: inherit;
  font-size: 1rem;
  padding: var(--space-3);
  background-color: var(--surface-card-nested);
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
}

.auth__hint {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

.auth__submit {
  margin-top: var(--space-2);
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-1);
  font-family: inherit;
  font-size: var(--text-ui);
  line-height: var(--text-ui-line);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: var(--space-3) var(--space-5);
  cursor: pointer;
}

.auth__submit:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.auth__error {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  border: var(--hairline) solid var(--border-strong);
  background-color: var(--surface-card-nested);
  color: var(--ink-1);
  font-size: var(--text-ui);
}

.auth__alt {
  margin: var(--space-5) 0 0;
  font-size: var(--text-ui);
  color: var(--ink-3);
}

/* Inline "Resend it" affordance — a real <button> (so it posts the
   known email) that reads as a text link, not a chunky CTA. */
.auth__linkbtn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-link);
  cursor: pointer;
}

.auth__linkbtn:hover {
  text-decoration: underline;
}

.auth__notice {
  margin: 0 0 var(--space-4);
  padding: var(--space-3);
  border: var(--hairline) solid var(--accent);
  color: var(--accent);
  font-size: var(--text-ui);
}

/* ---------- profile ---------- */
.profile__header {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-5);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.profile__name {
  margin: 0;
  font-size: var(--text-display);
  line-height: var(--text-display-line);
  font-weight: var(--text-display-weight);
  color: var(--ink-1);
}

.profile__subtitle {
  margin: var(--space-2) 0 0;
  color: var(--ink-3);
  font-size: var(--text-ui);
}

.profile__block {
  margin-bottom: var(--space-6);
}

.profile__section-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-ui);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-3);
  font-weight: var(--text-ui-weight);
  padding-bottom: var(--space-2);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.profile__badge {
  display: inline-block;
  font-size: 0.7em;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0 var(--space-2);
  border: var(--hairline) solid var(--border-strong);
  border-radius: 3px;
  margin-left: var(--space-1);
  vertical-align: middle;
}

.profile__badge--ok {
  color: var(--accent);
  border-color: var(--accent);
}

.profile__badge--warn {
  color: var(--ink-3);
}

.profile__meta {
  margin: 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--space-5);
  row-gap: var(--space-3);
  align-items: baseline;
}

.profile__meta dt {
  margin: 0;
  font-size: var(--text-ui);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-3);
}

.profile__meta dd {
  margin: 0;
  color: var(--ink-1);
}

.profile__stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
}

.profile__stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-4);
  border: var(--hairline) solid var(--border-subtle);
  border-radius: 4px;
  text-decoration: none;
  color: inherit;
  background: var(--surface-card-nested);
  transition: border-color 120ms ease, color 120ms ease;
}

a.profile__stat:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.profile__stat-num {
  font-size: var(--text-display);
  line-height: 1;
  font-weight: var(--text-display-weight);
  color: var(--ink-1);
}

a.profile__stat:hover .profile__stat-num {
  color: var(--accent);
}

.profile__stat-label {
  margin-top: var(--space-2);
  font-size: var(--text-ui);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}

.profile__logout {
  margin-top: var(--space-6);
  padding-top: var(--space-5);
  border-top: var(--hairline) solid var(--border-subtle);
}

.auth__signout {
  background: transparent;
  border: var(--hairline) solid var(--border-strong);
  color: var(--ink-3);
  font-family: inherit;
  font-size: var(--text-ui);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: var(--space-2) var(--space-4);
  cursor: pointer;
}

.auth__signout:hover {
  color: var(--ink-1);
}

/* ---------- account menu (header) ---------- */
.account-menu {
  position: relative;
}

.account-menu__summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-ui);
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
}

.account-menu__summary::-webkit-details-marker {
  display: none;
}

.account-menu__summary:hover {
  color: var(--ink-1);
}

.account-menu__name {
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-menu[open] .account-menu__chevron {
  transform: rotate(180deg);
}

.account-menu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: 20;
  min-width: 11rem;
  display: flex;
  flex-direction: column;
  background-color: var(--surface-card);
  border: var(--hairline) solid var(--border-strong);
}

.account-menu__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-ui);
  color: var(--ink-2);
  background: transparent;
  border: 0;
  font-family: inherit;
  cursor: pointer;
}

.account-menu__item:hover {
  background-color: var(--surface-card-nested);
  color: var(--ink-1);
}

.account-menu__signout {
  border-top: var(--hairline) solid var(--border-subtle);
}

/* ---------- lists page sections ---------- */
.lists-section {
  margin-top: var(--space-7);
}

.lists-section__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: var(--hairline) solid var(--border-subtle);
}

.lists-section__count {
  font-size: var(--text-meta);
  color: var(--ink-3);
}

/* Source: web/static/css/pages/not-found.css */
/* 404 / not-found page — the artwork carries the headline and tagline,
   so the surrounding chrome stays minimal: just real CTAs (the button
   in the image is illustrative, not clickable) and a quiet hint line. */
.not-found {
  display: grid;
  gap: var(--space-6);
  justify-items: center;
  text-align: center;
  margin: var(--space-8) auto var(--space-12);
  max-width: 720px;
}

.not-found__art {
  width: 100%;
  height: auto;
  border: var(--hairline) solid var(--border-strong);
  background-color: var(--surface-card);
}

.not-found__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
  justify-content: center;
}

.not-found__cta {
  background-color: var(--accent);
  border: var(--hairline) solid var(--accent);
  color: var(--surface-page);
  padding: var(--space-3) var(--space-6);
  font-size: var(--text-ui);
  font-weight: var(--text-ui-weight);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom: none;
}

.not-found__cta:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--surface-page);
  border-bottom: none;
}

.not-found__link {
  color: var(--accent-link);
  font-size: var(--text-ui);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  border-bottom-color: transparent;
}

.not-found__hint {
  max-width: 56ch;
}

/* Source: web/static/css/responsive.css */
/* ---------- responsive: narrow viewports ---------- */
@media (max-width: 720px) {
  .page {
    padding: var(--space-5) var(--space-6);
  }

  /* Mobile header: only brand (left) + hamburger (right) on row 1.
     Search, nav, and the theme toggle all collapse into the panel that
     unfurls when the hamburger is pressed — clears the most chrome
     before the page content (LOWEST PRICE etc.) starts. */
  .site-header {
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-5) 0 var(--space-6);
    margin-bottom: var(--space-8);
  }
  .nav-toggle {
    display: inline-flex;
    margin-left: auto;
  }

  /* Default state: hide search, nav, theme toggle. They're only present
     on mobile inside the hamburger panel. */
  .site-search,
  .site-nav,
  .watchlist-toggle,
  .theme-toggle {
    display: none;
  }

  /* Expanded state: the three sibling controls become a stacked panel
     below the top row, in the same source order as desktop but full
     width and vertically arranged. */
  .nav-toggle[aria-expanded="true"] ~ .site-search {
    display: block;
    order: 3;
    flex: 1 1 100%;
  }
  .nav-toggle[aria-expanded="true"] ~ .site-nav {
    display: flex;
    order: 4;
    flex-basis: 100%;
    flex-direction: column;
    gap: 0;
  }
  .nav-toggle[aria-expanded="true"] ~ .watchlist-toggle {
    display: inline-flex;
    align-items: center;
    order: 5;
    align-self: flex-start;
    min-height: 44px;
  }
  .nav-toggle[aria-expanded="true"] ~ .theme-toggle {
    display: inline-flex;
    align-items: center;
    order: 6;
    margin-left: 0;
    align-self: flex-start;
    min-height: 44px;
  }
  /* Inflate touch boxes to >=44px without changing visible text size.
     WCAG 2.5.5 / Apple HIG minimum; the 16px-tall nav links were
     mis-tapped repeatedly in mobile user-test sessions. */
  /* Inside the collapsed nav panel, links read as a stacked menu — give
     them a hairline divider and full-row touch targets. */
  .site-nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: var(--space-2) 0;
    border-bottom: var(--hairline) solid var(--border-subtle);
  }
  .site-nav a:last-child {
    border-bottom: none;
  }
  .theme-toggle {
    margin-left: auto;
    min-height: 44px;
  }

  /* Retailer CTA buttons in the Where-to-buy table need the same
     treatment — they were rendering ~34px tall, below the touch
     threshold. */
  .where-to-buy td:last-child a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Hero tile goes full-width on phones — see the 1023px block below, which
     stacks the header and stretches the tile to the column. */

  /* Catalog table → cards: handled in the (max-width: 1023px) block at the
     end of this file. It lives at 1023px (not 720px) because the table's
     intrinsic width (~650px) can't fit the two-column main track until the
     viewport is ~965px+, so tablets need the card layout too. */

  /* Spec table: row-headers don't need 14rem on a phone. */
  tbody th {
    width: auto;
    min-width: 9rem;
  }

  .hero h1 {
    font-size: 24px;
  }

  .hero__lede {
    font-size: 14px;
  }

  /* Stack the buy-promote on a phone so the CTA doesn't get crushed. */
  .buy-promote {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-5);
    padding: var(--space-5);
  }

  .buy-promote__cta {
    text-align: center;
  }

  /* Same column treatment for the action panel: on a phone the row of
     buttons becomes too wide to wrap nicely, so each button takes the
     full panel width and they stack instead. */
  .action-panel {
    padding: var(--space-5);
  }

  .action-panel__buttons {
    flex-direction: column;
    align-items: stretch;
  }

  .license-facts,
  .pricing-facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* On narrow viewports drop the textual labels so the icons + counts
   alone still fit in the header without wrapping. */
@media (max-width: 720px) {
  .favorites-toggle__label,
  .watchlist-link__label { display: none; }
}

/* ---------- descriptive tag groups: swipeable single-line rows ----------
   On a phone a category with several chips wraps into a tall stack. Instead
   keep each category to one line and let it scroll horizontally (native
   swipe). The per-edge fade is a hint that there's more — tag-scroll.js adds
   .can-scroll-left / .can-scroll-right only when the row actually overflows
   that way, so a short row that fits gets no fade at all. */
@media (max-width: 720px) {
  .tag-section .tag-row {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;           /* swipe is the gesture; no bar */
  }
  .tag-section .tag-row::-webkit-scrollbar { display: none; }
  .tag-section .tag-row > .tag { flex: 0 0 auto; }  /* chips keep their size */

  .tag-section .tag-row.can-scroll-right {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
  .tag-section .tag-row.can-scroll-left {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent);
            mask-image: linear-gradient(to left, #000 calc(100% - 28px), transparent);
  }
  .tag-section .tag-row.can-scroll-left.can-scroll-right {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
            mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent);
  }
}

/* ---------- catalog results: table → stacked cards below desktop ----------
   The plugin table's intrinsic width is ~650px (name + manufacturer + type +
   price + recommends). In the two-column catalog layout the results track
   only clears that at a ~965px+ viewport, so every width below desktop —
   phones AND tablets — overflowed the page, pushing the price and the
   recommend/favorite controls off-screen. Below 1024px each row becomes a
   self-contained card instead: a thumbnail rail on the left, name +
   manufacturer + category chips stacked, then price and the recommend/
   favorite actions on the bottom line. Desktop (>=1024px) keeps the dense,
   sortable table.

   The thead (per-column sort links) is visually hidden in card mode — the
   catalog's default "most recommended first" sort stands in; per-column sort
   stays a desktop affordance for now. */
@media (max-width: 1023px) {
  /* Collapse the 3-column hero into one stacked column on tablet/mobile:
     photo, then text + description, then features + resources.
     The accent stripe becomes a left border on the section itself, so it
     always runs the full stacked height (down to the bottom of external
     resources) — a `grid-row: 1 / -1` item would only span the first row
     once the grid has multiple implicit rows. */
  .identity {
    grid-template-columns: minmax(0, 1fr);
    border-left: 4px solid var(--accent);
    padding-left: var(--space-5);
  }
  .identity__accent {
    display: none;
  }
  .identity__body,
  .identity__features,
  .bundle-detail__image,
  .gui-tile--hero {
    grid-column: 1;
  }
  /* Once stacked, the hero tile stretches to the full content width. */
  .bundle-detail__image,
  .gui-tile--hero {
    width: 100%;
    max-width: none;
  }
  /* Tag groups go single-column when there isn't room for two. */
  .tag-section__groups {
    grid-template-columns: minmax(0, 1fr);
  }

  .plugin-list,
  .plugin-list tbody,
  .plugin-list tr,
  .plugin-list td {
    display: block;
  }
  .plugin-list thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    border: 0;
  }
  .plugin-list tr {
    display: grid;
    grid-template-columns: 56px 1fr auto;
    grid-template-areas:
      "thumb name  name"
      "thumb mfr   mfr"
      "thumb cats  cats"
      "thumb price actions";
    column-gap: var(--space-5);
    row-gap: var(--space-3);
    align-items: center;
    padding: var(--space-6);
    border: var(--hairline) solid var(--border-subtle);
    background-color: var(--surface-card);
  }
  .plugin-list tr + tr {
    margin-top: var(--space-5);
  }
  .plugin-list td {
    padding: 0;
    border: none;
  }

  /* Hover highlights the whole card, not each cell. Here the row is the grid
     container with its own background, so the shared desktop rule
     (tbody tr:hover td) would paint each cell block individually — leaving the
     grid gaps, the thumbnail column, and the card padding un-highlighted
     (patchy bands rather than a filled card). Move the highlight to the row
     and clear the per-cell paint so the whole card fills. */
  .plugin-list tbody tr:hover {
    background-color: var(--surface-card-nested);
  }
  .plugin-list tbody tr:hover td {
    background-color: transparent;
  }

  /* Bulk-select is a desktop power feature; drop it in card mode. */
  .plugin-list .col-select {
    display: none;
  }

  .plugin-list .thumb-cell {
    grid-area: thumb;
    align-self: start;
    width: 56px;
    padding: 0;
  }
  .plugin-list .thumb {
    width: 56px;
    height: 56px;
  }
  .plugin-list .col-name {
    grid-area: name;
    align-self: end;
  }
  /* Manufacturer rides along under the name now that there's room — the
     old narrow table had to drop it. */
  .plugin-list .col-mfr {
    grid-area: mfr;
    align-self: start;
  }
  .plugin-list .col-mfr__link {
    color: var(--ink-2);
    font-size: var(--text-meta);
  }
  .plugin-list .col-cats {
    grid-area: cats;
  }
  /* Both chips fit in a card — show the type chip the table had to hide. */
  .plugin-list .col-cats__tag {
    display: inline;
  }
  .plugin-list .num {
    grid-area: price;
    text-align: left;
    font-size: var(--text-h2);
  }
  .plugin-list .col-fav {
    grid-area: actions;
    width: auto;
    text-align: right;
  }
  /* Recommend/favorite/watchlist become primary touch actions — give them
     the 44px WCAG 2.5.5 / Apple-HIG target instead of the dense-table 28px. */
  .plugin-list .row-recommend,
  .plugin-list .row-favorite,
  .plugin-list .row-watchlist {
    min-height: 44px;
    min-width: 44px;
  }
}
