/* =============================================================================
   store.css — the public module store, the public changelog, and the extra
   badges the dashboard reuses for trust tiers.

   Every colour, size, radius, shadow and curve below comes from tokens.css,
   which base.html loads first on every page. Nothing here declares a raw hex or
   an arbitrary pixel gap: this file is the store's *layout and hierarchy*, not a
   second opinion about the palette. Where a value looks hardcoded it is because
   it is genuinely local geometry (a 148px preview strip, a 26% sidebar) that no
   other page shares.

   Load order is store.css last, on top of landing.css (public nav) and
   dashboard.css (shared cards, buttons, .markdown-body, the .tp theme tiles).
   So several rules here are deliberately narrow overrides of a dashboard
   default that is right in a dense admin list and wrong on a marketing page —
   the public changelog's long-form body being the clearest case.

   Sections
     1.  Page shell and hero
     2.  Trust legend
     3.  Module card grid
     4.  Theme preview on a card
     5.  Badges (trust tiers, version, state, classification)
     6.  Empty / error states
     7.  Public changelog
     8.  Long-form markdown on public pages
     9.  Code tabs and copy affordances
     10. Upload dropzone and forms
     11. Alerts
     12. Module detail — releases, actions, overflow menu, classification
   ============================================================================= */


/* =============================================================================
   1. PAGE SHELL AND HERO
   ============================================================================= */

.store-page {
  max-width: var(--mf-layout-max);
  margin: 0 auto;
  padding: var(--mf-space-7) var(--mf-space-5) var(--mf-space-9);
}

/* The hero is a masthead, not a landing page: one line of glow, centred type,
   and then out of the way. The radial sits behind the text rather than in a
   panel so there is no visible box edge fighting the cards below it. */
.store-head {
  position: relative;
  text-align: center;
  max-width: 760px;
  margin: 0 auto var(--mf-space-7);
  padding: var(--mf-space-6) var(--mf-space-4) var(--mf-space-5);
}

.store-head::before {
  content: "";
  position: absolute;
  inset: -20% -10% 20%;
  background: radial-gradient(ellipse 60% 70% at 50% 0%, var(--mf-accent-wash), transparent 70%);
  pointer-events: none;
  z-index: -1;
}

.store-head h1 {
  margin: 0 0 var(--mf-space-3);
  font-size: var(--mf-text-3xl);
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tighter);
  background: var(--mf-gradient-text);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.store-head p {
  max-width: var(--mf-layout-max-prose);
  margin: 0 auto var(--mf-space-4);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-lg);
  line-height: var(--mf-leading-normal);
}

.store-head p a {
  color: var(--mf-accent-text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* The repository URL. It is the one thing on this page a visitor has to carry
   away, so it is drawn as a real control — mono, bordered, obviously clickable
   — rather than as emphasised prose. */
.store-head code {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-2);
  min-height: var(--mf-control-height);
  margin-top: var(--mf-space-1);
  padding: var(--mf-space-2) var(--mf-space-4);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-full);
  color: var(--mf-accent2-text);
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-sm);
  word-break: break-all;
}


/* =============================================================================
   2. TRUST LEGEND
   Three cards explaining the badge tiers, on the page where the badges appear.
   Same construction as a module card so the page reads as one system.
   ============================================================================= */

.store-legend {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--mf-space-3);
  margin: 0 0 var(--mf-space-6);
  padding: 0;
  background: transparent;
  border: none;
}

.store-legend-card {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
  padding: var(--mf-space-4);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  transition: border-color var(--mf-dur-normal) var(--mf-ease),
              background-color var(--mf-dur-normal) var(--mf-ease);
}

.store-legend-card:hover {
  background: var(--mf-surface-raised);
  border-color: var(--mf-border-strong);
}

.store-legend-card .legend-desc {
  margin: 0;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}


/* =============================================================================
   2b. TYPE FILTER AND SECTIONS
   Modules and themes are not the same kind of thing and are no longer listed as
   though they were. The filter bar is a row of real in-page links that script
   upgrades into tabs -- see the comment in store.html for why that direction and
   not the other. Everything here has to look right in both states, so the bar is
   styled as a control group and never as a set of links.
   ============================================================================= */

.store-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-1);
  margin: var(--mf-space-7) 0 var(--mf-space-5);
  padding: var(--mf-space-1);
  /* A sunken tray with the pills raised out of it, rather than an underline tab
     bar: this sits between two full-width sections, and an underline would read
     as a divider belonging to the section beneath it. */
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-full);
  /* Only as wide as its contents. A three-item filter stretched across 1280px
     puts a third of a screen between "All" and "Themes". */
  width: fit-content;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.store-filter::-webkit-scrollbar {
  display: none;
}

.store-filter-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-shrink: 0;
  height: var(--mf-control-height);
  padding: 0 var(--mf-space-4);
  border-radius: var(--mf-radius-full);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-medium);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.store-filter-btn:hover {
  color: var(--mf-fg);
  background: var(--mf-surface-hover);
}

.store-filter-btn:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

/* `.is-active` and not `:target`: the script drives this, and :target would also
   fire on a plain anchor jump, leaving two tabs lit at once in the no-JS case. */
.store-filter-btn.is-active {
  color: var(--mf-fg);
  background: var(--mf-surface);
  box-shadow: var(--mf-shadow-xs);
}

/* The count is secondary to the label -- it says how many, not what. Tabular so
   the pill does not change width as the catalog grows past nine entries. */
.store-filter-count {
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--mf-fg-subtle);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  background: var(--badge-bg);
}

.store-filter-btn.is-active .store-filter-count {
  color: var(--mf-accent-text);
  background: var(--mf-accent-wash);
}

.store-section + .store-section {
  margin-top: var(--mf-space-8);
}

/* scroll-margin so that when the anchor *is* followed (no script, or a middle
   click), the heading does not land underneath the sticky public nav. */
.store-section {
  scroll-margin-top: var(--mf-space-8);
}

.store-section-head {
  margin-bottom: var(--mf-space-4);
}

.store-section-head h2 {
  margin: 0;
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-tight);
  color: var(--mf-fg);
}

.store-section-head p {
  margin: var(--mf-space-1) 0 0;
  max-width: 60ch;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}


/* =============================================================================
   3. MODULE CARD GRID
   The card is the product. Fixed anatomy top to bottom: preview (themes only),
   identity block, description clamped to three lines, facts, footer strip.
   Every card in a row is the same height and the footer sits on the baseline,
   which is what stops a grid of cards from looking like a list of files.
   ============================================================================= */

.store-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  gap: var(--mf-space-4);
  align-items: stretch;
}

/* The theme gallery: same card, narrower columns.

   A theme card carries a preview and barely any text -- no category, usually no
   "Requires", often no homepage -- so in a 320px module-sized column it is
   mostly empty space under a picture. Tightening the track lets more previews
   sit side by side, which is the comparison somebody choosing a theme is
   actually making. Nothing here changes the card itself; if it did, the two
   sections would start to diverge and the shared macro in store.html would stop
   being worth anything. */
.store-list-gallery {
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
}

.store-card {
  /* Declared once so the theme preview can pull itself back out of the padding
     without the two numbers drifting apart. */
  --store-card-pad: var(--mf-space-5);

  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--store-card-pad);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  overflow: hidden;
  transition: transform var(--mf-dur-normal) var(--mf-ease),
              border-color var(--mf-dur-normal) var(--mf-ease),
              box-shadow var(--mf-dur-normal) var(--mf-ease);
}

/* The accent rail that appears on hover. Above the theme preview, which is
   flush with the card's top edge and would otherwise paint over it. */
.store-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  width: 100%;
  height: 3px;
  background: var(--mf-gradient-brand);
  opacity: 0;
  transition: opacity var(--mf-dur-normal) var(--mf-ease);
}

/* Restrained: a 2px lift and a brighter edge. A glow on every card in a grid of
   thirty is not emphasis, it is noise. */
.store-card:hover {
  transform: translateY(-2px);
  border-color: var(--mf-border-strong);
  box-shadow: var(--mf-shadow-md);
}

.store-card:hover::before {
  opacity: 1;
}

.store-card-header {
  display: flex;
  align-items: flex-start;
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-3);
}

/* Initials tile. A module has no artwork to show, and inventing one would be a
   claim about the product; two letters on the brand wash is honest. */
.module-avatar {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  background: var(--mf-accent-wash);
  border: 1px solid var(--mf-border-accent);
  border-radius: var(--mf-radius-md);
  color: var(--mf-accent-text-bright);
  font-size: var(--mf-text-base);
  font-weight: var(--mf-weight-bold);
  letter-spacing: var(--mf-tracking-tight);
}

.store-card-title-area {
  flex: 1;
  min-width: 0;
}

.store-card h2 {
  margin: 0 0 var(--mf-space-2);
  color: var(--mf-fg);
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tight);
  /* Long module names truncate rather than wrapping to three lines and pushing
     the badges out of alignment with the card next door. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.store-card-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-1);
}

/* Exactly three lines, always — a one-line description and a five-line one must
   not produce two different card heights. */
.store-desc {
  margin: 0 0 var(--mf-space-4);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-normal);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Facts, as a definition list: label left, value right, one per line. Reads as a
   spec sheet, which is what a person comparing two modules is doing. */
.store-meta {
  margin: 0 0 var(--mf-space-4);
  padding-top: var(--mf-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  border-top: 1px solid var(--mf-border);
  font-size: var(--mf-text-sm);
}

.store-meta > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--mf-space-3);
}

.store-meta dt {
  flex-shrink: 0;
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-medium);
}

.store-meta dd {
  margin: 0;
  max-width: 62%;
  color: var(--mf-fg);
  font-weight: var(--mf-weight-medium);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.store-meta dd a {
  color: var(--mf-accent-text);
  text-decoration: none;
}

.store-meta dd a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Footer strip. `margin-top: auto` is what pins it to the bottom of a stretched
   grid cell; the negative side margins let it run edge to edge so it reads as
   part of the card's chrome rather than another paragraph. */
.store-hash {
  margin: auto calc(var(--store-card-pad) * -1) calc(var(--store-card-pad) * -1);
  padding: var(--mf-space-3) var(--store-card-pad);
  background: var(--mf-bg-sunken);
  border-top: 1px solid var(--mf-border);
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
  word-break: break-all;
}

.store-hash code {
  color: var(--mf-fg-muted);
  font-family: var(--mf-font-mono);
}

.store-hash a {
  color: var(--mf-accent-text);
  text-decoration: none;
  font-weight: var(--mf-weight-medium);
}

.store-hash a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}


/* =============================================================================
   4. THEME PREVIEW ON A STORE CARD
   dashboard.css draws the tile; this only places it and gives it an edge.

   The tile's colours come from inline style attributes written by the server
   from the theme's own palette (see templates/_theme_preview.html) — nothing
   here may set a background on .tp or its children, or a theme would be shown
   in colours it does not have. Geometry, radius, border and shadow only.
   ============================================================================= */

.store-card .tp {
  margin: calc(var(--store-card-pad) * -1) calc(var(--store-card-pad) * -1) var(--mf-space-4);
  height: 152px;
  border-radius: calc(var(--mf-radius-lg) - 1px) calc(var(--mf-radius-lg) - 1px) 0 0;
  border-bottom: 1px solid var(--mf-border-strong);
  /* A light theme's tile is nearly white and would otherwise bleed straight into
     the page. The inset hairline keeps its edge visible without tinting it. It is
     an ink hairline in *both* our palettes and deliberately not a token that
     flips: what it has to separate is the submitted theme's colours from the page,
     and a near-white tile is the case that needs help on a light page too. */
  box-shadow: inset 0 0 0 1px var(--mf-tile-edge);
}

/* The swatch row belongs to the preview above it, not to the facts below. */
.store-card .tp-facts {
  margin-bottom: var(--mf-space-4);
}


/* =============================================================================
   5. BADGES

   One construction for every badge in the app: pill, uppercase, 1px edge, wash
   behind status-coloured text. Only the colour changes, so a reader learns the
   shape once.

   The trust tiers are a security signal, so their hierarchy is built from more
   than hue — someone who cannot separate green from amber must still be able to
   tell them apart. Official is filled and bold with a solid border; verified is
   a wash with a visible border; unverified and pending are quiet and
   low-contrast, which is the correct visual weight for "nobody has checked
   this".
   ============================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  padding: 3px var(--mf-space-2);
  border: 1px solid transparent;
  border-radius: var(--mf-radius-full);
  background: var(--badge-bg);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-semibold);
  line-height: 1.5;
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  white-space: nowrap;
}

/* Highest tier. The only badge in the set with a saturated fill, because it is
   the only one that means "we wrote this". */
.badge-trust-official {
  background: var(--mf-ok-solid);
  border-color: var(--mf-ok-solid);
  color: var(--mf-fg-on-accent);
  font-weight: var(--mf-weight-bold);
}

.badge-trust-verified {
  background: var(--mf-accent-wash-strong);
  border-color: var(--mf-border-accent);
  color: var(--mf-accent-text-strong);
}

.badge-trust-unverified {
  background: var(--mf-warn-wash);
  border-color: var(--mf-warn-border);
  color: var(--mf-warn-fg);
}

/* `badge-trust-{{ r.trust }}` is templated, so a tier the store adds later still
   lands on a defined rule instead of the bare .badge default. */
.badge-trust-pending {
  background: var(--badge-bg);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg-subtle);
}

/* Version strings are data, not labels: mono, mixed case, tabular figures. */
.badge-version {
  background: var(--badge-bg);
  border-color: var(--mf-border);
  color: var(--mf-fg-muted);
  font-family: var(--mf-font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: var(--mf-weight-medium);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.badge-signed {
  background: var(--mf-ok-wash);
  border-color: var(--mf-ok-border);
  color: var(--mf-ok-fg);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

/* Is it actually being served? The one fact a store admin needs at a glance, and
   it earns a colour of its own — not the same green as "signed", because a
   signed module nobody can download and an unsigned one everybody can are very
   different situations. */
.badge-published {
  background: var(--mf-info-wash);
  border-color: var(--mf-info-border);
  color: var(--mf-info-fg);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.badge-unpublished {
  background: var(--badge-bg);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg-subtle);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.badge-broken {
  background: var(--mf-danger-wash);
  border-color: var(--mf-danger-border);
  color: var(--mf-danger-fg);
}

.badge-link {
  background: transparent;
  border-color: var(--mf-border);
  color: var(--mf-fg-subtle);
}

/* Type is a quiet structural fact; category gets the brand accent so it reads as
   the label people actually scan for. */
.badge-type-module {
  background: var(--badge-bg);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg-subtle);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.badge-type-template {
  background: var(--mf-accent2-wash);
  border-color: var(--mf-accent2-border);
  color: var(--mf-accent2-text);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.badge-category {
  background: var(--mf-accent-wash);
  border-color: var(--mf-border-accent);
  color: var(--mf-accent-text-strong);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}


/* =============================================================================
   6. EMPTY AND ERROR STATES
   Calm. An empty store is a young store, not a fault, and it should not be
   dressed up as one.
   ============================================================================= */

.store-error {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  margin: 0 0 var(--mf-space-5);
  padding: var(--mf-space-3) var(--mf-space-4);
  background: var(--mf-danger-wash);
  border: 1px solid var(--mf-danger-border);
  border-radius: var(--mf-radius-md);
  color: var(--mf-danger-fg);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-normal);
}

.store-empty {
  margin: var(--mf-space-7) 0;
  padding: var(--mf-space-7) var(--mf-space-5);
  background: var(--mf-surface);
  border: 1px dashed var(--mf-border-strong);
  border-radius: var(--mf-radius-lg);
  text-align: center;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-base);
}


/* =============================================================================
   7. PUBLIC CHANGELOG
   One entry per GitHub release. The tag and the date form a quiet mono column so
   the eye can run down them; the title carries the weight.
   ============================================================================= */

.changelog-list {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-4);
}

.changelog-entry {
  padding: var(--mf-space-5);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
}

.changelog-entry > header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin-bottom: var(--mf-space-4);
  padding-bottom: var(--mf-space-3);
  border-bottom: 1px solid var(--mf-border);
}

.changelog-entry > header h2 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  color: var(--mf-fg);
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-snug);
  letter-spacing: var(--mf-tracking-tight);
}

.changelog-entry > header h2 a {
  color: inherit;
  text-decoration: none;
}

.changelog-entry > header h2 a:hover {
  color: var(--mf-accent-text);
}

/* Pushed to the end of the row: the date is for orientation, not for reading. */
.changelog-date {
  margin-left: auto;
  color: var(--mf-fg-subtle);
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Entry-type chips inside a rendered release body, coloured from the shared
   --badge-* tokens so a "feature" is the same green everywhere in the product. */
.changelog-entry .badge-feature {
  background: var(--badge-feature-bg);
  color: var(--badge-feature-fg);
  border-color: currentColor;
}

.changelog-entry .badge-fix {
  background: var(--badge-fix-bg);
  color: var(--badge-fix-fg);
  border-color: currentColor;
}

.changelog-entry .badge-announcement {
  background: var(--badge-announcement-bg);
  color: var(--badge-announcement-fg);
  border-color: currentColor;
}

.changelog-entry .badge-warning {
  background: var(--badge-warning-bg);
  color: var(--badge-warning-fg);
  border-color: currentColor;
}

.changelog-entry .muted {
  margin: 0;
  font-size: var(--mf-text-md);
}


/* =============================================================================
   8. LONG-FORM MARKDOWN ON PUBLIC PAGES

   dashboard.css tunes .markdown-body for a preview inside a dense admin card:
   13px, headings one step below a card title, tight vertical rhythm. On the
   changelog it is the page's whole content and a visitor reads it end to end,
   so it gets body size, a real heading scale and room between blocks.

   The content is author-supplied and untrusted, so everything that can overflow
   is constrained: the measure, images, tables and pre blocks all stay inside the
   card no matter what someone pastes into a release note.
   ============================================================================= */

.store-page .markdown-body {
  max-width: 72ch;
  font-size: var(--mf-text-base);
  line-height: var(--mf-leading-relaxed);
  color: var(--mf-fg-muted);
  /* Not `overflow-x: auto` on the container: a wide table would then scroll the
     whole article. The offending elements scroll individually instead. */
  overflow-wrap: break-word;
}

.store-page .markdown-body h1,
.store-page .markdown-body h2,
.store-page .markdown-body h3 {
  margin: var(--mf-space-6) 0 var(--mf-space-3);
}

.store-page .markdown-body h4,
.store-page .markdown-body h5,
.store-page .markdown-body h6 {
  margin: var(--mf-space-5) 0 var(--mf-space-2);
}

.store-page .markdown-body h1 { font-size: var(--mf-text-2xl); }
.store-page .markdown-body h2 { font-size: var(--mf-text-xl); }
.store-page .markdown-body h3 { font-size: var(--mf-text-lg); }

.store-page .markdown-body h4,
.store-page .markdown-body h5,
.store-page .markdown-body h6 {
  font-size: var(--mf-text-md);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
}

.store-page .markdown-body p,
.store-page .markdown-body ul,
.store-page .markdown-body ol,
.store-page .markdown-body blockquote,
.store-page .markdown-body pre,
.store-page .markdown-body table {
  margin-bottom: var(--mf-space-4);
}

.store-page .markdown-body li {
  margin-bottom: var(--mf-space-2);
}

.store-page .markdown-body li::marker {
  color: var(--mf-fg-subtle);
}

.store-page .markdown-body blockquote {
  padding: var(--mf-space-3) var(--mf-space-4);
  border-left-width: 3px;
}

.store-page .markdown-body pre {
  padding: var(--mf-space-4);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

/* Long single-token lines (a URL, a stack frame) scroll rather than stretching
   the card past the grid column. */
.store-page .markdown-body pre code {
  white-space: pre;
  font-family: var(--mf-font-mono);
}

.store-page .markdown-body table {
  display: block;
  width: max-content;
  max-width: 100%;
  overflow-x: auto;
  font-size: var(--mf-text-sm);
}

.store-page .markdown-body img {
  max-width: 100%;
  height: auto;
  border: 1px solid var(--mf-border);
}


/* =============================================================================
   9. CODE TABS AND COPY AFFORDANCES
   The install snippets on the detail page: one panel, a tab strip, and a copy
   button that confirms itself in place.
   ============================================================================= */

.tabs-container {
  margin: var(--mf-space-4) 0;
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  overflow: hidden;
}

.tab-headers {
  display: flex;
  background: var(--mf-bg-sunken);
  border-bottom: 1px solid var(--mf-border);
  overflow-x: auto;
}

.tab-btn {
  flex-shrink: 0;
  height: var(--mf-control-height-lg);
  padding: 0 var(--mf-space-4);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--mf-fg-muted);
  font: inherit;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.tab-btn:hover {
  color: var(--mf-fg);
  background: var(--mf-surface-hover);
}

.tab-btn.active {
  color: var(--mf-fg);
  background: var(--mf-surface);
  border-bottom-color: var(--mf-accent);
}

.tab-btn:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

.tab-panel {
  display: none;
  position: relative;
}

.tab-panel.active {
  display: block;
}

.code-block-wrapper {
  position: relative;
  background: var(--mf-bg-sunken);
}

/* The wrapper owns the frame, so the <pre> inside it must not draw a second one.
   `!important` because the markdown styles it inherits are more specific. */
.code-block-wrapper pre {
  margin: 0 !important;
  border: none !important;
  border-radius: 0 !important;
  background: transparent !important;
}

.copy-btn {
  position: absolute;
  top: var(--mf-space-2);
  right: var(--mf-space-2);
  z-index: 5;
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  height: var(--mf-control-height-sm);
  padding: 0 var(--mf-space-3);
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-sm);
  color: var(--mf-fg-muted);
  font: inherit;
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  cursor: pointer;
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease);
}

.copy-btn:hover {
  background: var(--mf-surface-active);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg);
}

.copy-btn:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

.copy-btn.copied {
  background: var(--mf-ok-wash);
  border-color: var(--mf-ok-solid);
  color: var(--mf-ok-fg);
}

/* Any inline value that copies on click — an ID, a hash, the store URL. The
   tooltip is the only thing that says so, so it has to be reliable. */
.copyable-badge {
  position: relative;
  cursor: pointer;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.copyable-badge::after {
  content: "Copy";
  position: absolute;
  bottom: calc(100% + var(--mf-space-2));
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  padding: var(--mf-space-1) var(--mf-space-2);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-xs);
  color: var(--mf-fg);
  font-family: var(--mf-font-sans);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-medium);
  letter-spacing: var(--mf-tracking-normal);
  text-transform: none;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--mf-dur-fast) var(--mf-ease),
              transform var(--mf-dur-fast) var(--mf-ease);
}

.copyable-badge:hover::after,
.copyable-badge:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

.copyable-badge.copied::after {
  content: "Copied";
  background: var(--mf-ok-solid);
  border-color: var(--mf-ok-solid);
  color: var(--mf-fg-on-accent);
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}


/* =============================================================================
   10. UPLOAD DROPZONE AND FORMS (dashboard side)
   ============================================================================= */

.upload-dropzone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--mf-space-3);
  padding: var(--mf-space-6) var(--mf-space-5);
  background: var(--mf-bg-sunken);
  border: 2px dashed var(--mf-border-strong);
  border-radius: var(--mf-radius-lg);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--mf-dur-normal) var(--mf-ease),
              background-color var(--mf-dur-normal) var(--mf-ease);
}

.upload-dropzone:hover {
  background: var(--mf-surface);
  border-color: var(--mf-accent);
}

/* The real input covers the whole zone, so a click anywhere opens the picker and
   a drop anywhere lands on it. */
.upload-dropzone input[type="file"] {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.upload-dropzone-icon {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-full);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xl);
  transition: background-color var(--mf-dur-normal) var(--mf-ease),
              color var(--mf-dur-normal) var(--mf-ease),
              border-color var(--mf-dur-normal) var(--mf-ease);
}

.upload-dropzone:hover .upload-dropzone-icon {
  background: var(--mf-accent-wash);
  border-color: var(--mf-border-accent);
  color: var(--mf-accent-text);
}

.upload-dropzone-text {
  margin: 0;
  color: var(--mf-fg);
  font-size: var(--mf-text-base);
  font-weight: var(--mf-weight-semibold);
}

.upload-dropzone-sub {
  margin: 0;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-sm);
}

/* Shown by the picker's change handler once a file is chosen. */
.upload-selected-file {
  display: none;
  margin-top: var(--mf-space-1);
  color: var(--mf-accent-text);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-medium);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--mf-space-4);
  padding: var(--mf-space-5);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
}

.form-grid label {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
}

.form-grid .wide { grid-column: 1 / -1; }

.form-grid input[type="text"],
.form-grid input[type="url"],
.form-grid input[type="number"],
.form-grid input[type="file"] {
  height: var(--mf-control-height);
  padding: 0 var(--mf-space-3);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  color: var(--mf-fg);
  font: inherit;
  font-weight: var(--mf-weight-normal);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

/* A file input's button needs the vertical room the fixed height takes away. */
.form-grid input[type="file"] {
  height: auto;
  padding: var(--mf-space-2) var(--mf-space-3);
}

.form-grid input:focus-visible {
  outline: none;
  border-color: var(--mf-focus-color);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.muted {
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-normal);
}


/* =============================================================================
   11. ALERTS
   Same shape three times over; only the wash and the edge change, so severity is
   the only thing that reads as different.
   ============================================================================= */

.info-alert,
.warning-alert,
.danger-alert {
  margin-bottom: var(--mf-space-5);
  padding: var(--mf-space-4);
  border: 1px solid transparent;
  border-radius: var(--mf-radius-md);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-normal);
}

.info-alert {
  background: var(--mf-accent-wash);
  border-color: var(--mf-border-accent);
}

.warning-alert {
  background: var(--mf-warn-wash);
  border-color: var(--mf-warn-border);
}

.danger-alert {
  background: var(--mf-danger-wash);
  border-color: var(--mf-danger-border);
}

.info-alert strong,
.warning-alert strong,
.danger-alert strong {
  color: var(--mf-fg);
}


/* =============================================================================
   12. MODULE DETAIL — RELEASES
   The per-module page groups its releases into the live "main" version, any
   pending update set apart in amber, and older versions folded into a dropdown.
   Each release is a self-contained card with a collapsible changelog and a
   grouped, roomy action bar.
   ============================================================================= */

/* A label above a group of versions: quiet, uppercase, out of the way. */
.rel-section-label {
  margin: var(--mf-space-5) 2px var(--mf-space-2);
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
}

.rel-section-label:first-child { margin-top: var(--mf-space-1); }

/* One release. The left rail colour is keyed to the part it plays in the module,
   so the live version is identifiable before a word has been read. */
.release {
  padding: var(--mf-space-4) var(--mf-space-5);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-left: 3px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-md);
}

.release.is-live {
  border-left-color: var(--mf-ok-solid);
  background: linear-gradient(90deg, var(--mf-ok-wash), transparent 40%), var(--mf-surface);
}

.release.is-waiting {
  border-left-color: var(--mf-warn-solid);
  background: linear-gradient(90deg, var(--mf-warn-wash), transparent 45%), var(--mf-surface);
}

.release.is-previous { border-left-color: var(--mf-track-strong); }

.release + .release { margin-top: var(--mf-space-3); }

.release-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--mf-space-4);
}

.release-title {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
  margin: 0;
  font-weight: var(--mf-weight-semibold);
}

.release-title .ver {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-lg);
  font-weight: var(--mf-weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--mf-tracking-tight);
}

.release-badges {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-1);
}

/* The facts under a release heading. This used to be a flat run of comma-less spans —
   a hash, a size, a date, a name, a count — which reads as one long sentence with no
   subject, and which was where every fact demoted out of the badge row would have landed
   if it had stayed a run. Two columns with labels: the label says which question the value
   answers, and a value can be missing without leaving a gap you have to decode. */
.rfacts {
  display: grid;
  grid-template-columns: minmax(min(96px, 100%), auto) minmax(0, 1fr);
  gap: 4px var(--mf-space-4);
  margin: var(--mf-space-3) 0 0;
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-normal);
}

.rfacts dt {
  color: var(--mf-fg-subtle);
  text-transform: uppercase;
  font-size: var(--mf-text-2xs);
  letter-spacing: var(--mf-tracking-wider);
  padding-top: 2px;
}

.rfacts dd {
  margin: 0;
  color: var(--mf-fg-muted);
  overflow-wrap: anywhere;
}

.rfacts dd.num,
.rfacts .num { font-variant-numeric: tabular-nums; }
.rfacts code { font-family: var(--mf-font-mono); }

/* Inline note under a release (unsigned reason, decline reason). */
.release-note {
  margin-top: var(--mf-space-3);
  padding: var(--mf-space-2) var(--mf-space-3);
  background: var(--mf-bg-sunken);
  border-left: 3px solid var(--mf-border-strong);
  border-radius: 0 var(--mf-radius-sm) var(--mf-radius-sm) 0;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

.release-note.is-error {
  background: var(--mf-danger-wash);
  border-left-color: var(--mf-danger-solid);
  color: var(--mf-danger-fg);
}

.release-note strong { color: inherit; }

/* Collapsible changelog — native <details>, closed unless the release is a
   pending update, in which case the reviewer needs to read it first. */
details.changelog {
  margin-top: var(--mf-space-3);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  overflow: hidden;
}

details.changelog > summary {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-2) var(--mf-space-3);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  list-style: none;
  cursor: pointer;
  user-select: none;
  transition: color var(--mf-dur-fast) var(--mf-ease);
}

details.changelog > summary:hover { color: var(--mf-fg); }
details.changelog > summary::-webkit-details-marker { display: none; }

details.changelog > summary .rev-tag {
  margin-left: auto;
  color: var(--mf-fg-subtle);
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-normal);
}

/* Author-written text, rendered as-is: `pre-wrap` keeps their line breaks, and
   the indent lines it up under the summary's label rather than its chevron. */
.changelog-body {
  padding: 0 var(--mf-space-3) var(--mf-space-3) var(--mf-space-6);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

.changelog-body a {
  display: inline-block;
  margin-top: var(--mf-space-2);
  color: var(--mf-accent-text);
  text-decoration: none;
}

.changelog-body a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Review box for a pending release: says what state it is in, and offers the one
   non-destructive verdict. Declining lives below it, in its own fold. */
.review-box {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin-top: var(--mf-space-3);
  padding-top: var(--mf-space-3);
  border-top: 1px solid var(--mf-border);
}

.review-box form { margin: 0; }

.review-box-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-right: auto;
}

.review-box-label {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-subtle);
}

.review-box-meta {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-muted);
}

/* ---- Decline panel ------------------------------------------------------
   Folded shut by default: it is the destructive verdict, and it should cost a
   deliberate tap rather than sit next to Approve. Open, the reason gets a full
   width textarea — the reviewer is writing to a person and needs to see the
   whole message before sending it. */
.decline-fold {
  margin-top: var(--mf-space-3);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  background: var(--mf-surface);
}

.decline-fold > summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
  min-height: 44px;
  padding: var(--mf-space-2) var(--mf-space-3);
  border-radius: var(--mf-radius-md);
  list-style: none;
  cursor: pointer;
  user-select: none;
  transition: background-color var(--mf-dur-fast) var(--mf-ease);
}

.decline-fold > summary::-webkit-details-marker { display: none; }

.decline-fold > summary:hover { background: var(--mf-danger-wash); }

.decline-fold > summary:focus-visible {
  outline: 2px solid var(--mf-focus-color);
  outline-offset: -2px;
}

.decline-fold[open] > summary {
  border-bottom: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md) var(--mf-radius-md) 0 0;
}

.decline-fold-title {
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-danger-fg);
}

.decline-fold-hint {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

.review-decline {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  padding: var(--mf-space-3);
}

.decline-label {
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-muted);
}

.review-decline textarea {
  width: 100%;
  box-sizing: border-box;
  min-height: 92px;
  resize: vertical;
  padding: var(--mf-space-2) var(--mf-space-3);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

.review-decline textarea:focus-visible {
  outline: none;
  border-color: var(--mf-focus-color);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.decline-actions {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
}

.decline-hint {
  flex: 1 1 240px;
  margin: 0;
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-normal);
  color: var(--mf-fg-subtle);
}

/* Action toolbar: buttons grouped by purpose, with a thin divider between groups
   and room to breathe. Wraps cleanly instead of squeezing every button onto one
   crowded line. */
.action-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin-top: var(--mf-space-4);
  padding-top: var(--mf-space-4);
  border-top: 1px solid var(--mf-border);
}

.action-group {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
}

.action-group form { margin: 0; }

.action-group + .action-group {
  position: relative;
  padding-left: var(--mf-space-3);
}

.action-group + .action-group::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 1px;
  height: 20px;
  background: var(--mf-border-strong);
}

.action-spacer { flex: 1 1 auto; }

/* A divider makes no sense flush against the spacer's edge. */
.action-spacer + .action-group { padding-left: 0; }
.action-spacer + .action-group::before { display: none; }

/* Older-versions dropdown — collapsed by default. */
details.older { margin-top: var(--mf-space-2); }

details.older > summary {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-3) var(--mf-space-4);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  list-style: none;
  cursor: pointer;
  user-select: none;
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

details.older > summary:hover {
  background: var(--mf-surface-hover);
  border-color: var(--mf-border-strong);
}

details.older > summary::-webkit-details-marker { display: none; }

details.older > summary .count {
  margin-left: auto;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
  font-variant-numeric: tabular-nums;
  font-weight: var(--mf-weight-normal);
}

.older-body { padding-top: var(--mf-space-3); }

/* The shared chevron: rotates when its parent <details> is open. */
.chev {
  display: inline-flex;
  color: var(--mf-fg-subtle);
  transition: transform var(--mf-dur-normal) var(--mf-ease);
}

.chev svg { width: 16px; height: 16px; display: block; }
details[open] > summary .chev { transform: rotate(90deg); }

/* Channel + rollout + button in one row overflows a narrow card even above the
   phone breakpoint; let it wrap rather than clip. */
.channel-group form { flex-wrap: wrap; }
.channel-select { min-width: 92px; }

/* ---- "More" overflow menu on a release action bar ------------------------
   Keeps the action bar down to a couple of visible buttons; the occasional and
   destructive actions live behind this native <details> menu. Closing on an
   outside click is wired up in module_detail.html. */
.menu { position: relative; }
.menu > summary { list-style: none; }
.menu > summary::-webkit-details-marker { display: none; }
.menu-trigger { user-select: none; }

.menu-trigger .caret {
  display: inline-flex;
  transition: transform var(--mf-dur-normal) var(--mf-ease);
}

.menu-trigger .caret svg { width: 14px; height: 14px; display: block; }
details.menu[open] .menu-trigger .caret { transform: rotate(180deg); }

.menu-pop {
  position: absolute;
  right: 0;
  /* Opens upward. A release row sits low in a long version list, so a menu that
     dropped down was normally cut off by the viewport and had to be scrolled to.
     Above the trigger there is always the rest of the page. */
  bottom: calc(100% + var(--mf-space-1));
  z-index: var(--mf-z-dropdown);
  /* Wide enough for the classification block, which is the roomiest thing that
     can appear in here. */
  min-width: 210px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--mf-space-1);
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-md);
  box-shadow: var(--mf-shadow-lg);
}

.menu-pop form { margin: 0; }

/* Every entry — button, link or form submit — reads as one uniform menu row. */
.menu-item {
  width: 100%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-2) var(--mf-space-3);
  background: transparent;
  border: none;
  border-radius: var(--mf-radius-xs);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-medium);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease);
}

.menu-item:hover { background: var(--mf-surface-hover); }
.menu-item.is-danger { color: var(--mf-danger-fg); }
.menu-item.is-danger:hover { background: var(--mf-danger-wash); }

.menu-sep {
  height: 1px;
  margin: var(--mf-space-1) 0;
  background: var(--mf-border);
}

/* ---- Type & category: classification card ------------------------------- */
.classify-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--mf-space-3);
}

.classify-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--mf-space-3);
  margin-top: var(--mf-space-3);
}

.classify-field {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  font-size: var(--mf-text-sm);
}

.classify-field > span {
  color: var(--mf-fg-muted);
  font-weight: var(--mf-weight-medium);
}

.classify-field select {
  width: 100%;
  box-sizing: border-box;
  height: var(--mf-control-height);
  padding: 0 var(--mf-space-3);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-md);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.classify-field select:focus-visible {
  outline: none;
  border-color: var(--mf-focus-color);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.classify-field select:disabled { opacity: 0.5; cursor: not-allowed; }

/* Reserves its line whether or not there is help text, so choosing a category
   does not shove the rest of the card down. */
.classify-help {
  margin: var(--mf-space-2) 0 0;
  min-height: 1.1em;
  line-height: var(--mf-leading-normal);
}

/* ---- Type & category editor inside the "More" menu ----------------------- */
.menu-classify {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  padding: var(--mf-space-1) var(--mf-space-2) 2px;
}

.menu-classify-label {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
}

.menu-classify-label .settings-hint {
  font-weight: var(--mf-weight-medium);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
}

.menu-classify select {
  width: 100%;
  box-sizing: border-box;
  height: var(--mf-control-height-sm);
  padding: 0 var(--mf-space-2);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-xs);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
}

.menu-classify select:focus-visible {
  outline: none;
  border-color: var(--mf-focus-color);
}

.menu-classify select:disabled { opacity: 0.5; cursor: not-allowed; }
.menu-classify-save { width: 100%; margin-top: 2px; }


/* =============================================================================
   RESPONSIVE
   ============================================================================= */

@media (max-width: 720px) {
  .store-page { padding: var(--mf-space-6) var(--mf-space-4) var(--mf-space-8); }
  .store-head { padding: var(--mf-space-4) 0 var(--mf-space-3); }
  .store-head h1 { font-size: var(--mf-text-2xl); }
  .store-head p { font-size: var(--mf-text-base); }
  .store-card { --store-card-pad: var(--mf-space-4); }
  .changelog-entry { padding: var(--mf-space-4); }

  /* Both are two-column layouts that have nowhere left to put a second column. */
  .store-meta,
  .form-grid { grid-template-columns: 1fr; }

  /* The date has no row left to be pushed to the end of once the header wraps. */
  .changelog-date { margin-left: 0; }
}

@media (max-width: 640px) {
  .action-bar { flex-direction: column; align-items: stretch; }
  .action-group { width: 100%; }
  .action-group + .action-group { padding-left: 0; padding-top: var(--mf-space-2); }
  .action-group + .action-group::before { display: none; }
  .action-group .btn { flex: 1; }
  .action-spacer { display: none; }

  /* The channel controls are one logical row (which channel, at what percentage)
     and read worse stacked than laid out as a small grid: select and percentage
     share the first line, the button takes the second. */
  .channel-group form {
    display: grid;
    grid-template-columns: 1fr 76px;
    gap: var(--mf-space-2);
  }

  .channel-group .btn { grid-column: 1 / -1; }
  .channel-group .rollout-input { width: 100%; }

  /* Yank pairs a free-text reason with a destructive button. Side by side that
     leaves the field about forty pixels wide. (Decline is already a column.) */
  .menu-yank {
    flex-direction: column;
    align-items: stretch;
    gap: var(--mf-space-2);
  }

  /* One thumb, one column: the hint above, a full-width button below, so the
     destructive action is never a narrow target squeezed against text. */
  .decline-actions { flex-direction: column; align-items: stretch; }
  .decline-actions .btn { width: 100%; }

  /* The review box stacks: state on its own line, Approve full width under it. */
  .review-box { flex-direction: column; align-items: stretch; }
  .review-box-head { margin-right: 0; }
  .review-box form,
  .review-box .btn { width: 100%; }

  /* On a stacked action bar the menu spans the row. Still opens upward. */
  .menu, .menu-trigger { width: 100%; }
  .menu-pop { left: 0; right: 0; min-width: 0; }
}


/* =============================================================================
   PUBLIC STORE — OWNER ROW, TAGS, FILTER SUMMARY
   =============================================================================
   The card grew four facts it never carried: who owns the listing, which
   repository it is published from, its channel, and its tags. Owner and repo sit
   on one line under the description because they answer the same question — "who
   is behind this, and can I go and look" — and splitting them across two rows made
   a card that was already tall taller for no gain.
   ========================================================================== */

.store-owner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mf-space-2);
  margin: 0 0 var(--mf-space-3);
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
}

.store-owner strong { color: var(--mf-fg); font-weight: var(--mf-weight-semibold); }

/* Initials, not an avatar image. The store has no account pictures and inventing
   one — a generated identicon, a gravatar lookup — would put a stranger's face on
   somebody's module or leak an email hash to a third party. */
.store-owner-av {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-accent);
  color: var(--mf-fg-on-brand);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  letter-spacing: var(--mf-tracking-wide);
}

/* Unclaimed reads as absence, not as an error. Nobody did anything wrong by not
   claiming a module, and a red badge would say they had. */
.store-owner-av.is-none {
  background: var(--mf-track);
  color: var(--mf-fg-muted);
}

.store-owner-none { color: var(--mf-fg-subtle); font-style: italic; }
.store-owner-none code { font-style: normal; }

.store-repo {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  color: var(--mf-accent-text);
  text-decoration: none;
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  overflow-wrap: anywhere;
}

.store-repo:hover { text-decoration: underline; }
.store-repo:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); border-radius: var(--mf-radius-xs); }

.store-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 var(--mf-space-3);
}

/* A tag is a search you can click, so it is a link and looks like one you may
   press — not a decorative pill that turns out to be inert. */
.store-tag {
  padding: 2px var(--mf-space-2);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-full);
  background: var(--mf-surface);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-2xs);
  text-decoration: none;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease);
}

.store-tag:hover { border-color: var(--mf-border-accent); color: var(--mf-accent-text); }
.store-tag:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); }

.store-filter-summary {
  margin: 0 0 var(--mf-space-5);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
}

.store-filter-summary a { color: var(--mf-accent-text); }

/* A visual break between two facet families in one chip row (type, then trust).
   A separator rather than a second row: the two together are still one decision —
   "narrow the shelf" — and stacking them made the page look like it had three
   filter bars. */
.catalog-chip-sep {
  align-self: stretch;
  width: 1px;
  margin: 0 var(--mf-space-1);
  background: var(--mf-border);
}

.catalog-chip-label {
  align-self: center;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
}

.catalog-categories { margin-bottom: var(--mf-space-5); }

/* Numbers that change between two renders — counts, installs, versions — are
   tabular so a column of them does not jitter as digits change width. */
.num,
.store-card .store-meta dd.num,
.vver,
.chip-n.num { font-variant-numeric: tabular-nums; }


/* =============================================================================
   OVERVIEW — PULSE STRIP
   =============================================================================
   A row of figure cells, each a fact somebody arrives with a question about.
   Anything not known prints as an em dash, because "0" and "we have no figure"
   look identical and mean opposites.

   The module detail page used to open with five of these and no longer does — see
   MODULE DETAIL — HEADER below for the quieter three-fact line that replaced them.
   This block stays because the Overview is a page whose entire job *is* figures,
   which is the one place a wall of them is the right answer.
   ========================================================================== */

.pulse-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-5);
}

.pulse-cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  background: var(--mf-surface);
}

.pulse-cell.is-alert {
  border-color: var(--mf-warn-fg);
  background: var(--mf-warn-wash);
}

.pulse-l {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
}

.pulse-v {
  color: var(--mf-fg);
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-tight);
  overflow-wrap: anywhere;
}

.pulse-v.num { font-variant-numeric: tabular-nums; }
.pulse-v-sm { font-size: var(--mf-text-md); }
.pulse-v a { color: var(--mf-accent-text); text-decoration: none; }
.pulse-v a:hover { text-decoration: underline; }
.pulse-v a:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); border-radius: var(--mf-radius-xs); }

/* "unclaimed", "not linked". Subdued and italic: an absent fact, not a failure. */
.pulse-none {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-normal);
}

.pulse-cell.is-alert .pulse-v { color: var(--mf-warn-fg); }

.pulse-d {
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-snug);
}


/* =============================================================================
   MODULE DETAIL — HEADER, QUALITY LINE, SECTION TABS
   =============================================================================
   The page's whole answer to "aktuell ist es so überlaufen". Nothing was deleted;
   it was ranked. One header, three facts, one line of listing quality, and four
   sections behind links.

   The rule this block follows throughout: a hairline and a heading, not another
   bordered box. The old page nested cards three deep, and a border drawn around
   something that is already visually a unit adds noise without adding a boundary.
   ========================================================================== */

.mod-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--mf-space-3) var(--mf-space-4);
  margin-bottom: var(--mf-space-4);
}

.mod-head-main { min-width: 0; }
.mod-head-main h1 { margin: 2px 0 0; font-size: var(--mf-text-2xl); }

.mod-head-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
}

.mod-sub {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin: var(--mf-space-2) 0 0;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
}

.mod-sub code {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  overflow-wrap: anywhere;
}

.mod-sub .num { font-variant-numeric: tabular-nums; }

/* Three facts, one line, no boxes. The five-cell version of this was a grid of
   bordered tiles with 20px numbers, which gave five equal-weight objects the
   visual mass of the page's actual subject. Hairline dividers do the same
   separating job at a fraction of the ink. */
.mfacts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-3) 0;
  margin: 0 0 var(--mf-space-4);
  padding: var(--mf-space-3) 0;
  border-top: 1px solid var(--mf-border);
  border-bottom: 1px solid var(--mf-border);
}

.mfact {
  flex: 1 1 min(180px, 100%);
  min-width: 0;
  padding: 0 var(--mf-space-4);
  border-left: 1px solid var(--mf-border);
}

.mfact:first-child { padding-left: 0; border-left: 0; }

.mfact-l {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
}

.mfact-v {
  margin: 2px 0 0;
  color: var(--mf-fg);
  font-size: var(--mf-text-lg);
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-tight);
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.mfact-d {
  margin: 2px 0 0;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-snug);
  overflow-wrap: anywhere;
}

.mfact-d.num { font-variant-numeric: tabular-nums; }

/* Attention is the one fact allowed to raise its voice, and it does it with colour
   on the figure alone — not a filled tile. A warning that repaints a whole cell
   makes the other two facts look switched off. */
.mfact.is-alert .mfact-v { color: var(--mf-warn-fg); }
.mfact.is-alert .mfact-l { color: var(--mf-warn-fg); }

/* ---- Listing quality, in one line ---------------------------------------
   The panel's headline and its point, as a link into the section that holds the
   working list. It is deliberately not a card: a to-do list drawn as a dashboard
   panel reads as a verdict on the module, which is the one thing listing quality
   is not allowed to be. */
.qline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin: 0 0 var(--mf-space-5);
  padding: var(--mf-space-3) 0;
  border-bottom: 1px solid var(--mf-border);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  text-decoration: none;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.qline:hover { color: var(--mf-fg); border-bottom-color: var(--mf-border-accent); }
.qline:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); border-radius: var(--mf-radius-xs); }

.qline-label {
  color: var(--mf-fg);
  font-weight: var(--mf-weight-semibold);
}

/* The shared .meter, shrunk and un-stacked. */
.qline-meter {
  flex: 1 1 min(140px, 100%);
  max-width: 220px;
  height: 6px;
  margin: 0;
}

.qline-score {
  font-variant-numeric: tabular-nums;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

.qline-go { margin-left: auto; color: var(--mf-accent-text); white-space: nowrap; }
.qline.is-warn .qline-go { color: var(--mf-warn-fg); }

/* ---- Section tabs --------------------------------------------------------
   Links, styled as a tab strip. Horizontally scrollable rather than wrapping: a
   two-line tab strip on a phone costs more vertical room than the header above
   it, and the four labels are short enough that a swipe reaches the last one. */
.mtabs {
  display: flex;
  gap: var(--mf-space-1);
  margin: 0 0 var(--mf-space-5);
  border-bottom: 1px solid var(--mf-border);
  overflow-x: auto;
  scrollbar-width: none;
}

.mtabs::-webkit-scrollbar { display: none; }

.mtab {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex: none;
  height: var(--mf-control-height-lg);
  padding: 0 var(--mf-space-3);
  border-bottom: 2px solid transparent;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  white-space: nowrap;
  text-decoration: none;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.mtab:hover { color: var(--mf-fg); }
.mtab:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); border-radius: var(--mf-radius-xs); }

.mtab.is-active {
  color: var(--mf-fg);
  border-bottom-color: var(--mf-accent);
}

/* A count, or a single warning mark. Never both, and never a count of zero — a
   badge that is always lit stops meaning anything. */
.mtab-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-bg-sunken);
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
  font-variant-numeric: tabular-nums;
}

.mtab-n.is-warn { background: var(--mf-warn-wash); color: var(--mf-warn-fg); }
.mtab.is-active .mtab-n { color: var(--mf-fg-muted); }
.mtab.is-active .mtab-n.is-warn { color: var(--mf-warn-fg); }

/* ---- Sections ------------------------------------------------------------
   All four are in the document; the server marks one active and the other three
   are hidden here. That is the opposite of the version panes below, and for the
   opposite reason: the *server* has already chosen, so a page whose script never
   runs still shows exactly what the URL asked for. */
.msec { display: none; }
.msec.is-active { display: block; }
.msec[hidden] { display: none; }

.msec-head { margin-bottom: var(--mf-space-4); }
.msec-head h2 { margin: 0; font-size: var(--mf-text-lg); }
.msec-head .settings-hint { margin: var(--mf-space-2) 0 0; max-width: 68ch; }

/* The two facts the header stopped carrying, restated once at the top of the
   section that can act on them. */
.msec-summary { margin: 0 0 var(--mf-space-5); }

/* A sub-section: a heading, a rule, content. This replaces .card for everything
   on this page — two cards side by side inside a fold inside the page was three
   nested borders to separate two headings. */
.msub { margin: 0 0 var(--mf-space-6); }

.msub-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin: 0 0 var(--mf-space-3);
  padding-bottom: var(--mf-space-2);
  border-bottom: 1px solid var(--mf-border);
}

.msub-head h2,
.msub-head h3 { margin: 0; font-size: var(--mf-text-md); }


/* =============================================================================
   MODULE DETAIL — FOLDS, KEY/VALUE LISTS, OWNERSHIP
   ========================================================================== */

.fold {
  margin: var(--mf-space-4) 0 0;
  border-top: 1px solid var(--mf-border);
  padding-top: var(--mf-space-3);
}

.fold > summary {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  cursor: pointer;
  list-style: none;
  color: var(--mf-fg);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
}

.fold > summary::-webkit-details-marker { display: none; }
.fold > summary:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); border-radius: var(--mf-radius-xs); }
.fold[open] > summary .chev svg { transform: rotate(90deg); }
.fold-body { padding-top: var(--mf-space-3); }

.card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-3);
}

.card-head h2,
.card-head h3 { margin: 0; font-size: var(--mf-text-md); }
.card-head h2 { font-size: var(--mf-text-lg); }

.kv {
  display: grid;
  grid-template-columns: minmax(min(140px, 100%), auto) 1fr;
  gap: 4px var(--mf-space-3);
  margin: var(--mf-space-3) 0 0;
  font-size: var(--mf-text-sm);
}

.kv dt { color: var(--mf-fg-subtle); }
.kv dd { margin: 0; color: var(--mf-fg-muted); overflow-wrap: anywhere; }

.owner-list { list-style: none; margin: 0; padding: 0; }

.owner-item {
  display: flex;
  align-items: flex-start;
  gap: var(--mf-space-3);
  padding: var(--mf-space-3) 0;
  border-bottom: 1px solid var(--mf-border);
}

.owner-item:last-child { border-bottom: 0; }
.owner-avatar { width: 34px; height: 34px; font-size: var(--mf-text-2xs); }
.owner-body { flex: 1 1 auto; min-width: 0; }
.owner-body strong { display: block; }

.owner-role {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.owner-role form { margin: 0; }

.owner-grant,
.owner-claim {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mf-space-2);
  margin-top: var(--mf-space-4);
  padding-top: var(--mf-space-3);
  border-top: 1px solid var(--mf-border);
}

.owner-grant input[type="text"],
.owner-grant select {
  min-width: 0;
  flex: 1 1 140px;
  height: var(--mf-control-height-sm);
  padding: 0 var(--mf-space-2);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-xs);
  background: var(--mf-bg-sunken);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
}

.owner-grant input:focus-visible,
.owner-grant select:focus-visible {
  outline: none;
  border-color: var(--mf-focus-color);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.owner-grant .settings-hint,
.owner-claim .settings-hint { flex: 1 1 100%; margin: 0; }

.event-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
}

.event-list li { padding: 4px 0; border-bottom: 1px solid var(--mf-border); }
.event-list li:last-child { border-bottom: 0; }

.event-when {
  display: inline-block;
  min-width: 90px;
  color: var(--mf-fg-subtle);
  font-variant-numeric: tabular-nums;
}

/* The dashboard module list gained an owner column; a module with no owner says so
   in the same subdued voice the store page uses. */
.is-unclaimed { color: var(--mf-fg-subtle); font-style: italic; }


/* =============================================================================
   MODULE DETAIL — LISTING QUALITY
   =============================================================================
   Three buckets, a meter, and a footer that says plainly that none of it blocks
   anything. The colours are deliberately warn/neutral/ok and never danger: a thin
   description is not a defect, and a red panel would make people either argue with
   it or learn to ignore it.
   ========================================================================== */

.meter {
  display: flex;
  height: 8px;
  margin-bottom: var(--mf-space-4);
  border-radius: var(--mf-radius-full);
  background: var(--mf-bg-sunken);
  overflow: hidden;
}

.meter span { height: 100%; }
.meter .m-ok { background: var(--mf-ok-solid); }
.meter .m-warn { background: var(--mf-warn-solid); }
.meter .m-gap { background: var(--mf-track); }

.q-group { margin-bottom: var(--mf-space-4); }
.q-group .eyebrow { display: flex; align-items: center; gap: 6px; margin-bottom: var(--mf-space-2); }

.q-item {
  display: flex;
  align-items: flex-start;
  gap: var(--mf-space-3);
  padding: var(--mf-space-3) 0;
  border-top: 1px solid var(--mf-border);
}

.q-ico {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: var(--mf-radius-full);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
}

.q-ico.is-warn { background: var(--mf-warn-wash); color: var(--mf-warn-fg); }
.q-ico.is-skip { background: var(--mf-bg-sunken); color: var(--mf-fg-subtle); }
.q-ico.is-ok { background: var(--mf-ok-wash); color: var(--mf-ok-fg); }

.q-body { flex: 1 1 auto; min-width: 0; }
.q-body b { display: block; font-size: var(--mf-text-sm); }

.q-why {
  display: block;
  margin-top: 2px;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-normal);
}

/* The value the heuristic is objecting to, printed verbatim. This is the line that
   makes a false positive cheap: the author reads it, disagrees in one glance, and
   moves on instead of hunting for what the panel meant. */
.q-val {
  display: inline-block;
  margin-top: 6px;
  padding: 2px var(--mf-space-2);
  border-radius: var(--mf-radius-xs);
  background: var(--mf-bg-sunken);
  color: var(--mf-fg-muted);
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-2xs);
  overflow-wrap: anywhere;
}

.q-act { flex: none; }
.q-act .settings-hint { margin: 0; font-size: var(--mf-text-2xs); }

.q-complete-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 0 var(--mf-space-4);
}

.quality-footer {
  margin: var(--mf-space-4) 0 0;
  padding-top: var(--mf-space-3);
  border-top: 1px solid var(--mf-border);
}


/* =============================================================================
   MODULE DETAIL — VERSION MASTER / DETAIL
   =============================================================================
   Mobile first, and that is not a formality here: the list is a full-width column
   above the pane on a phone and a fixed sidebar beside it from 900px up. The
   two-column form is the enhancement, not the baseline.

   `.is-enhanced` is added by script. Without it every pane stays visible, so a page
   whose JavaScript failed is a long page rather than a version list with nothing
   under it.
   ========================================================================== */

.vsplit {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--mf-space-4);
}

.vlist {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  /* Capped on a phone, where the list sits *above* the pane: a module with twelve
     versions would otherwise push the version you are looking at entirely off the
     first screen, and the pane is the point. The cap is lifted at 900px, where the
     list becomes a sidebar and costs the pane no vertical room at all.
     `overscroll-behavior` stops a flick inside the list from scrolling the page
     once it hits the end, which is the thing that makes nested scrolling feel
     broken rather than merely compact. */
  max-height: 44vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.vrow {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-2) var(--mf-space-3);
  border: 1px solid transparent;
  border-radius: var(--mf-radius-md);
  background: none;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.vrow:hover { background: var(--mf-surface-hover); }
.vrow:focus-visible { outline: none; box-shadow: var(--mf-focus-ring); }

.vrow.is-selected {
  border-color: var(--mf-border-accent);
  background: var(--mf-accent-wash);
}

/* A 3px rail keyed to what the version *is*. The badge on the right says the same
   thing in words — the rail is there so a list of ten is scannable without
   reading, and the words are there because a colour on its own is not a label. */
.vrail {
  flex: none;
  width: 3px;
  align-self: stretch;
  min-height: 28px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-track);
}

.vrow.is-live .vrail { background: var(--mf-ok-solid); }
.vrow.is-waiting .vrail { background: var(--mf-info-solid); }
.vrow.is-pending .vrail { background: var(--mf-warn-solid); }
.vrow.is-yanked .vrail,
.vrow.is-declined .vrail { background: var(--mf-danger-solid); }

.vmeta { flex: 1 1 auto; min-width: 0; }

.vver {
  display: block;
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  font-variant-numeric: tabular-nums;
}

.vsub {
  display: block;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-2xs);
  line-height: var(--mf-leading-snug);
  overflow-wrap: anywhere;
}

.vlist-note { margin: var(--mf-space-3) 0 0; }

.vpanes { min-width: 0; }

.vpane {
  min-width: 0;
  padding: var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  background: var(--mf-bg-sunken);
  /* scroll-margin so that following #rel-42 with no script lands the pane below the
     sticky topbar instead of under it. */
  scroll-margin-top: calc(var(--mf-layout-topbar) + var(--mf-space-4));
}

.vpane + .vpane { margin-top: var(--mf-space-3); }

/* Only once script has said it may. See the section comment. */
.vsplit.is-enhanced .vpane { display: none; }
.vsplit.is-enhanced .vpane.is-active { display: block; margin-top: 0; }
.vsplit.is-enhanced .vpane[hidden] { display: none; }

.vhead {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--mf-space-2) var(--mf-space-3);
  margin-bottom: var(--mf-space-3);
}

.vhead .ver {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-bold);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 900px) {
  /* The list is a fixed rail; the pane takes the rest. minmax(0, 1fr) on the second
     track is what stops a long sha256 or a wide check grid from widening the whole
     grid instead of scrolling inside it. */
  .vsplit {
    grid-template-columns: minmax(min(240px, 100%), 260px) minmax(0, 1fr);
    align-items: start;
  }

  .vlist {
    position: sticky;
    top: calc(var(--mf-layout-topbar) + var(--mf-space-4));
    max-height: calc(100vh - var(--mf-layout-topbar) - var(--mf-space-8));
    overflow-y: auto;
  }
}

@media (max-width: 720px) {
  .vpane { padding: var(--mf-space-3); }
  .pulse-cell { padding: var(--mf-space-3); }

  .mod-head-actions { width: 100%; }
  .mod-head-actions .btn { flex: 1 1 min(140px, 100%); }
  .mod-head-main h1 { font-size: var(--mf-text-xl); }

  /* Three facts side by side at 360px leaves each of them about a hundred pixels,
     which is not enough for "1 waiting for review". Two up, one under, and the
     dividers follow. */
  .mfact { flex-basis: min(140px, 45%); padding: 0 var(--mf-space-3); }
  .mfact:nth-child(odd) { padding-left: 0; border-left: 0; }

  /* The count moves under the meter rather than off the end of the line. */
  .qline-go { margin-left: 0; flex-basis: 100%; }

  /* Two columns of key/value have nowhere left to go; the label sits above its
     value instead of being squeezed to four characters. Same for a release's
     facts, whose labels are wider than its values are short. */
  .kv { grid-template-columns: 1fr; gap: 0 0; }
  .kv dt { margin-top: var(--mf-space-2); }
  .rfacts { grid-template-columns: 1fr; gap: 0; }
  .rfacts dt { margin-top: var(--mf-space-2); }

  /* The fix affordance drops under the finding rather than fighting it for width. */
  .q-item { flex-wrap: wrap; }
  .q-act { width: 100%; padding-left: calc(18px + var(--mf-space-3)); }
  .q-act .btn { width: 100%; }
}
