/* Design tokens live in tokens.css, which base.html loads before this file for
   every page in the app. The `:root` block that used to sit here has been
   removed rather than moved: three stylesheets each declaring their own copy of
   the palette is precisely how they drifted apart. Everything it defined is
   still available under the same names -- see the compatibility section at the
   bottom of tokens.css -- so no rule below had to change to keep working. */

/* The changelog badge tokens that used to sit here have moved to tokens.css, with the
   rest of the palette. They were left behind — declarations with no selector and a
   stray closing brace — when the old `:root` block was removed, which is exactly the
   kind of quiet breakage that motivated putting the palette in one file: four of them
   (--badge-important-* and --badge-release-*) were referenced by _release_row.html and
   defined nowhere the browser could see. */
/* =============================================================================
   dashboard.css

   The admin shell and every screen inside it. Ordered so that a rule is always
   below the thing it depends on:

     1.  Base / document
     2.  Flash messages
     3.  Shell layout (sidebar, topbar, content)
     4.  Buttons
     5.  Cards, grids, stats
     6.  Page header
     7.  Post list, markdown preview
     8.  Badges, status, empty states
     9.  Forms and controls
     10. Admin rows
     11. Download counts
     12. Catalog toolbar
     13. Theme previews
     14. Module list (KPIs, avatars, sparklines)
     15. Review checks and review queue
     16. Audit log
     17. Tables
     18. Findings board
     19. Responsive corrections

   Two conventions worth stating, because breaking either of them is invisible
   until it is everywhere:

   - No raw values. Sizes, colours, radii, shadows and easings come from the
     --mf-* scale. If a value looks like it needs a new step, it almost always
     needs the nearest existing one instead.
   - Transitions name their properties. A blanket transition over every
     property animates layout properties nobody intended to animate, and the
     cost only shows up on the slowest device in the fleet.
   ============================================================================= */

/* =============================================================================
   1. BASE
   ============================================================================= */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--mf-font-sans);
  background: var(--mf-bg);
  color: var(--mf-fg);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-normal);
  min-height: 100vh;
}

a {
  color: inherit;
}

/* =============================================================================
   2. FLASH MESSAGES
   Fixed, centred, stacked. Calm by construction: a tinted hairline border and a
   matching text tone, no filled block. A flash that fills with saturated colour
   competes with the page it is reporting on.
   ============================================================================= */

.flash-container {
  position: fixed;
  top: var(--mf-space-4);
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--mf-z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  width: min(92vw, 420px);
}

.flash {
  padding: var(--mf-space-3) var(--mf-space-4);
  border-radius: var(--mf-radius-md);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-snug);
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border-strong);
  box-shadow: var(--mf-shadow-md);
}

.flash-error   { border-color: var(--mf-danger-fg); background: var(--mf-danger-wash); color: var(--mf-danger-fg); }
.flash-success { border-color: var(--mf-ok-fg);     background: var(--mf-ok-wash);     color: var(--mf-ok-fg); }
.flash-info    { border-color: var(--mf-info-fg);   background: var(--mf-info-wash);   color: var(--mf-info-fg); }
.flash-warning { border-color: var(--mf-warn-fg);   background: var(--mf-warn-wash);   color: var(--mf-warn-fg); }

/* =============================================================================
   3. SHELL LAYOUT
   ============================================================================= */

.dashboard-shell {
  display: flex;
  min-height: 100vh;
}

.sidebar {
  width: var(--mf-layout-sidebar);
  flex-shrink: 0;
  background: var(--mf-surface);
  border-right: 1px solid var(--mf-border);
  padding: var(--mf-space-5) var(--mf-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-5);
  position: sticky;
  top: 0;
  height: 100vh;
  z-index: var(--mf-z-sticky);
  /* The menu is taller than the viewport on a phone, and was taller than a short laptop
     window once it grew from six entries to thirteen plus headings. `height: 100vh` without
     this simply clipped the rest — no scrollbar, no way to reach Settings.

     `overscroll-behavior: contain` stops the scroll chaining into the page behind the
     drawer on mobile, which is what makes an open menu feel like it is fighting you. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

/* iOS/Android browsers shrink the visible viewport when the URL bar is showing, so 100vh is
   taller than the screen and the last menu entry sits under the chrome. dvh follows the
   *dynamic* viewport. Kept as a progressive enhancement: browsers without it keep the
   100vh above, which now scrolls anyway. */
@supports (height: 100dvh) {
  .sidebar {
    height: 100dvh;
  }
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  font-weight: var(--mf-weight-bold);
  font-size: var(--mf-text-md);
  letter-spacing: var(--mf-tracking-tight);
  padding: 0 var(--mf-space-2);
  text-decoration: none;
  color: var(--mf-fg);
}

.brand-mark {
  width: 28px;
  height: 28px;
  border-radius: var(--mf-radius-sm);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* A small drop shadow rather than the old glow: enough to separate the mark
     from the sidebar, not enough to look like it is lit from behind. */
  filter: drop-shadow(0 2px 4px rgba(124, 92, 255, 0.35));
}

.brand-mark svg { width: 24px; height: 24px; }

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Nothing in the sidebar may shrink. This is the line that makes the menu scroll.

   A flex item defaults to `flex-shrink: 1`, so when the menu is taller than the sidebar,
   the browser's first move is to *compress the items* until they fit — not to overflow.
   Nothing overflows, so `overflow-y: auto` above has nothing to scroll, and the visible
   symptom is not a missing scrollbar: it is squashed content. The short items give way
   first, which is why the section headings were the ones sliced in half.

   With shrinking off, the content keeps its natural height, the sidebar overflows, and
   the scrollbar appears. */
.sidebar > *,
.sidebar-nav > * {
  flex-shrink: 0;
}

/* The rail on the active entry is drawn with a pseudo-element rather than an
   inset box-shadow so it can be inset from the top and bottom edges — a rail
   that runs the full height of the row reads as a border on the sidebar, not as
   a marker on the item. */
.sidebar-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-2) var(--mf-space-3);
  border-radius: var(--mf-radius-sm);
  color: var(--mf-fg-muted);
  text-decoration: none;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-medium);
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease);
}

.sidebar-link svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  opacity: 0.75;
  transition: opacity var(--mf-dur-fast) var(--mf-ease);
}

.sidebar-link:hover {
  background: var(--mf-surface-hover);
  color: var(--mf-fg);
}

.sidebar-link:hover svg {
  opacity: 1;
}

.sidebar-link.active {
  background: var(--mf-accent-wash);
  color: var(--mf-fg);
  font-weight: var(--mf-weight-semibold);
}

.sidebar-link.active svg {
  opacity: 1;
  color: var(--mf-accent-text);
}

.sidebar-link.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 2px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-accent);
}

.sidebar-badge {
  margin-left: auto;
  background: var(--mf-warn-wash);
  color: var(--mf-warn-fg);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-wide);
  line-height: 1.5;
  padding: 0 var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  font-variant-numeric: tabular-nums;
}

.sidebar-footer {
  margin-top: auto;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  padding: 0 var(--mf-space-2);
}

.main-area {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.topbar {
  height: var(--mf-layout-topbar);
  border-bottom: 1px solid var(--mf-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--mf-space-5);
  position: sticky;
  top: 0;
  /* color-mix keeps the translucent bar tied to the theme's background rather
     than to a hardcoded near-black, so it survives the light palette. */
  background: color-mix(in srgb, var(--mf-bg) 85%, transparent);
  backdrop-filter: blur(12px);
  z-index: var(--mf-z-sticky);
}

.topbar-title {
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-md);
  letter-spacing: var(--mf-tracking-tight);
}

.topbar-user {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
}

.user-chip {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
}

.user-avatar {
  width: 28px;
  height: 28px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-gradient-brand);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  color: var(--mf-fg-on-accent);
}

.hamburger {
  display: none;
  background: none;
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-sm);
  width: var(--mf-control-height);
  height: var(--mf-control-height);
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--mf-fg);
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.hamburger:hover {
  background: var(--mf-surface-hover);
  border-color: var(--mf-border-strong);
}

.hamburger span,
.hamburger span::before,
.hamburger span::after {
  content: "";
  display: block;
  width: 16px;
  height: 2px;
  border-radius: var(--mf-radius-full);
  background: currentColor;
  position: relative;
}

.hamburger span::before { position: absolute; top: -5px; }
.hamburger span::after { position: absolute; top: 5px; }

/* =============================================================================
   4. BUTTONS
   One height (--mf-control-height), one padding, one radius. Hover is a colour
   change rather than a lift: rows of buttons that translate on hover make a
   dense admin table twitch as the pointer crosses it.
   ============================================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--mf-space-2);
  height: var(--mf-control-height);
  padding: 0 var(--mf-control-padding-x);
  border-radius: var(--mf-radius-sm);
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-sm);
  line-height: 1;
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  font-family: inherit;
  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),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

.btn-primary {
  background: var(--mf-gradient-brand);
  color: var(--mf-fg-on-accent);
  box-shadow: var(--mf-shadow-xs);
}

.btn-primary:hover {
  box-shadow: var(--mf-shadow-brand);
  filter: brightness(1.06);
}

.btn-primary:active {
  filter: brightness(0.96);
  box-shadow: none;
}

.btn-ghost {
  background: transparent;
  border-color: var(--mf-border-strong);
  color: var(--mf-fg);
}

.btn-ghost:hover {
  background: var(--mf-surface-hover);
  border-color: var(--mf-border-strong);
}

.btn-ghost:active {
  background: var(--mf-surface-active);
}

.btn-danger {
  background: transparent;
  border-color: color-mix(in srgb, var(--mf-danger-fg) 45%, transparent);
  color: var(--mf-danger-fg);
}

.btn-danger:hover {
  background: var(--mf-danger-wash);
  border-color: var(--mf-danger-fg);
}

.btn-danger:active {
  background: color-mix(in srgb, var(--mf-danger-fg) 22%, transparent);
}

.btn-sm {
  height: var(--mf-control-height-sm);
  padding: 0 var(--mf-space-3);
  font-size: var(--mf-text-xs);
}

.content {
  padding: var(--mf-space-5);
  flex: 1;
}

/* =============================================================================
   5. CARDS, GRIDS, STATS
   ============================================================================= */

.card {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  padding: var(--mf-space-5);
  box-shadow: var(--mf-shadow-sm);
}

.overview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-6);
}

/* The little arrow on a menu entry that leaves the dashboard. Pushed to the right edge and
   kept quiet — it is a promise about what the click will do, not decoration. */
.sidebar-link .sidebar-external {
  width: 13px;
  height: 13px;
  margin-left: auto;
  opacity: 0.45;
  flex-shrink: 0;
  transition: opacity var(--mf-dur-fast) var(--mf-ease);
}

.sidebar-link:hover .sidebar-external {
  opacity: 0.8;
}

/* Visible to a screen reader, to nobody else: the icon says "new tab" to people who can see
   it, and this says the same thing to people who can't. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* A module card in the overview is a link into the detail page. It should feel like one —
   respond on hover — without turning into a different component. The response is a
   surface and border change rather than a transform, so a grid of them does not
   jump as the pointer moves across it. */
.module-summary {
  display: flex;
  transition: background-color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.module-summary:hover {
  background: var(--mf-surface-raised);
  border-color: var(--mf-border-strong);
}

.module-summary:hover .btn {
  border-color: var(--mf-border-accent);
}

/* ---- overview widgets, notification settings ---------------------------- */

/* A number without a unit is a riddle. Every stat card gets one line saying what it
   counts, in the smallest type on the page — there when you wonder, invisible when you
   don't. */
.stat-card .stat-hint {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-snug);
  margin-top: var(--mf-space-1);
}

.stat-card .stat-label {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  margin-bottom: var(--mf-space-2);
}

.stat-card .stat-value {
  font-size: var(--mf-text-2xl);
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tight);
  font-variant-numeric: tabular-nums;
  background: var(--mf-gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* Only the review queue gets a colour, and only when it isn't zero: a dashboard where
   everything shouts is a dashboard where nothing does. */
.stat-card-warn {
  border-color: color-mix(in srgb, var(--mf-warn-fg) 40%, transparent);
  background: var(--mf-warn-wash);
}

.section-heading {
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  margin: var(--mf-space-5) 0 var(--mf-space-3);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: var(--mf-space-4);
  margin-top: var(--mf-space-3);
}

.form-grid label {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
}

.toggle-row {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  margin: var(--mf-space-4) 0 var(--mf-space-2);
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-md);
}

/* The event list. Checkboxes with their explanation right there — a member choosing what
   reaches them should not have to guess what "module_signed" means. */
.event-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: var(--mf-space-2);
  margin-top: var(--mf-space-2);
}

.event-item {
  display: flex;
  align-items: flex-start;
  gap: var(--mf-space-2);
  padding: var(--mf-space-2) var(--mf-space-3);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  font-size: var(--mf-text-sm);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.event-item:hover {
  border-color: var(--mf-border-strong);
  background: var(--mf-surface-raised);
}

.event-item span {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.event-item .settings-hint {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

/* An empty slot that explains itself. Dashed, unfilled, and quiet — it must not
   read as a card that failed to load. */
.placeholder-card {
  border: 1px dashed var(--mf-border-strong);
  border-radius: var(--mf-radius-lg);
  padding: var(--mf-space-7) var(--mf-space-5);
  text-align: center;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-sm);
}

.placeholder-card h3 {
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-lg);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-tight);
  margin: var(--mf-space-3) 0 var(--mf-space-1);
}

/* ---------------- Coming Soon (standard/self-registered accounts) ---------------- */
.coming-soon-card {
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-xl);
  padding: var(--mf-space-9) var(--mf-space-5);
  text-align: center;
  background: var(--mf-surface);
  box-shadow: var(--mf-shadow-md);
}

.coming-soon-badge {
  display: inline-flex;
  align-items: center;
  height: var(--mf-control-height-sm);
  padding: 0 var(--mf-space-3);
  border-radius: var(--mf-radius-full);
  border: 1px solid var(--mf-border-accent);
  background: var(--mf-accent-wash);
  color: var(--mf-accent-text);
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  margin-bottom: var(--mf-space-4);
}

.coming-soon-card h2 {
  font-size: var(--mf-text-2xl);
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-tight);
  margin: 0 0 var(--mf-space-3);
  letter-spacing: var(--mf-tracking-tight);
  background: var(--mf-gradient-brand);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.coming-soon-card p {
  color: var(--mf-fg-muted);
  max-width: 420px;
  margin: 0 auto;
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-relaxed);
}

/* =============================================================================
   6. PAGE HEADER
   The top of the hierarchy: page title > section heading > card title > body >
   meta. Nothing below this file's h1 may be larger than it.
   ============================================================================= */

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-5);
}

.page-header h1 {
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-tight);
  margin: 0;
  letter-spacing: var(--mf-tracking-tight);
}

.page-header p {
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-snug);
  margin: var(--mf-space-1) 0 0;
}

/* =============================================================================
   7. POST LIST AND MARKDOWN PREVIEW
   ============================================================================= */

.post-list {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
}

.post-row {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  padding: var(--mf-space-4) var(--mf-space-5);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--mf-space-4);
  flex-wrap: wrap;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.post-row:hover {
  border-color: var(--mf-border-strong);
}

.post-row-main {
  flex: 1;
  min-width: 220px;
}

.post-row-title {
  font-weight: var(--mf-weight-semibold);
  margin: 0 0 var(--mf-space-1);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-snug);
  letter-spacing: var(--mf-tracking-tight);
}

.post-row-body {
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
  margin: 0 0 var(--mf-space-2);
  white-space: pre-wrap;
  max-width: 60ch;
}

/* ---------------- Rendered Markdown preview (dashboard list) ---------------- */
.markdown-body {
  max-width: 70ch;
  overflow-x: auto;
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

.markdown-body > *:first-child {
  margin-top: 0;
}

.markdown-body > *:last-child {
  margin-bottom: 0;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  color: var(--mf-fg);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tight);
  margin: var(--mf-space-4) 0 var(--mf-space-2);
}

/* Rendered markdown lives *inside* a card, so its headings start one step below
   the page title rather than competing with it. */
.markdown-body h1 { font-size: var(--mf-text-lg); }
.markdown-body h2 { font-size: var(--mf-text-md); }
.markdown-body h3 { font-size: var(--mf-text-md); }
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 { font-size: var(--mf-text-sm); }

.markdown-body p {
  margin: 0 0 var(--mf-space-2);
}

.markdown-body a {
  color: var(--mf-accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.markdown-body strong { color: var(--mf-fg); font-weight: var(--mf-weight-semibold); }

.markdown-body ul,
.markdown-body ol {
  margin: 0 0 var(--mf-space-2);
  padding-left: 1.4em;
}

.markdown-body li {
  margin-bottom: var(--mf-space-1);
}

.markdown-body blockquote {
  margin: 0 0 var(--mf-space-2);
  padding: var(--mf-space-1) var(--mf-space-3);
  border-left: 2px solid var(--mf-accent);
  background: var(--mf-surface-raised);
  border-radius: 0 var(--mf-radius-sm) var(--mf-radius-sm) 0;
  color: var(--mf-fg-muted);
}

.markdown-body code {
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-xs);
  padding: 1px 5px;
  font-size: 0.9em;
  font-family: var(--mf-font-mono);
  color: var(--mf-accent-text);
}

.markdown-body pre {
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-3);
  overflow-x: auto;
  margin: 0 0 var(--mf-space-2);
}

.markdown-body pre code {
  background: none;
  border: none;
  padding: 0;
  color: var(--mf-fg);
}

.markdown-body hr {
  border: none;
  border-top: 1px solid var(--mf-border);
  margin: var(--mf-space-4) 0;
}

.markdown-body table {
  border-collapse: collapse;
  width: 100%;
  margin: 0 0 var(--mf-space-2);
  font-size: var(--mf-text-xs);
}

.markdown-body th,
.markdown-body td {
  border: 1px solid var(--mf-border);
  padding: var(--mf-space-1) var(--mf-space-2);
  text-align: left;
}

.markdown-body th {
  background: var(--mf-surface-raised);
  color: var(--mf-fg);
  font-weight: var(--mf-weight-semibold);
}

.markdown-body img {
  max-width: 100%;
  border-radius: var(--mf-radius-sm);
}

.post-row-meta {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  font-variant-numeric: tabular-nums;
}

.post-row-actions {
  display: flex;
  gap: var(--mf-space-2);
  flex-shrink: 0;
}

/* =============================================================================
   8. BADGES, STATUS, EMPTY STATES
   One badge construction: pill radius, wash background, matching text tone from
   the status tokens. Every variant below sets colours only — if a variant needs
   a different size or shape, it is a different component.
   ============================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  padding: 2px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-semibold);
  line-height: 1.6;
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  white-space: nowrap;
}

.badge-feature { background: var(--badge-feature-bg); color: var(--badge-feature-fg); }
.badge-fix { background: var(--badge-fix-bg); color: var(--badge-fix-fg); }
.badge-announcement { background: var(--badge-announcement-bg); color: var(--badge-announcement-fg); }
.badge-warning { background: var(--badge-warning-bg); color: var(--badge-warning-fg); }
.badge-important { background: var(--badge-important-bg); color: var(--badge-important-fg); box-shadow: inset 0 0 0 1px var(--mf-danger-border); }
.badge-release { background: var(--badge-release-bg); color: var(--badge-release-fg); }
/* The version number itself, shown next to the type badge on release rows. */
.badge-release-tag {
  background: var(--bg-surface-2);
  color: var(--text-secondary);
  font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}

.status-dot {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  font-size: var(--mf-text-xs);
}

.status-dot::before {
  content: "";
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-radius: var(--mf-radius-full);
  background: currentColor;
}

.status-dot.is-published {
  color: var(--mf-ok-fg);
}

.status-dot.is-published::before {
  background: var(--mf-ok-fg);
}

.status-dot.is-draft {
  color: var(--mf-fg-subtle);
}

/* An empty state is a normal, expected screen — most of these lists start empty.
   Centred, generously spaced, and in the muted tone rather than an alarm colour. */
.empty-state {
  text-align: center;
  padding: var(--mf-space-8) var(--mf-space-5);
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

/* =============================================================================
   9. FORMS AND CONTROLS
   Every field is the same height, the same radius and the same focus treatment.
   Focus uses the accent border plus a soft ring so it is visible against both
   the sunken and the raised surface.
   ============================================================================= */

.form-card {
  max-width: 820px;
  width: 100%;
}

/* ---------------- Settings page ---------------- */
.settings-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--mf-space-5);
  margin-bottom: var(--mf-space-5);
}

.settings-row-text h3 {
  margin: 0 0 var(--mf-space-2);
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-tight);
}

.settings-row-text p {
  margin: 0;
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
  max-width: 60ch;
}

.settings-row-control {
  flex-shrink: 0;
}

.toggle-switch {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  cursor: pointer;
  user-select: none;
}

.toggle-switch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-switch-track {
  position: relative;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
  border-radius: var(--mf-radius-full);
  background: var(--mf-surface-active);
  border: 1px solid var(--mf-border-strong);
  transition: background-color var(--mf-dur-normal) var(--mf-ease),
              border-color var(--mf-dur-normal) var(--mf-ease);
}

.toggle-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-fg-muted);
  box-shadow: var(--mf-shadow-xs);
  transition: transform var(--mf-dur-normal) var(--mf-ease-out),
              background-color var(--mf-dur-normal) var(--mf-ease);
}

/* On = the brand accent. The old amber read as a warning on a control whose only
   meaning is "this setting is enabled". */
.toggle-switch input:checked + .toggle-switch-track {
  background: var(--mf-accent-wash-strong);
  border-color: var(--mf-border-accent);
}

.toggle-switch input:checked + .toggle-switch-track .toggle-switch-thumb {
  transform: translateX(20px);
  background: var(--mf-accent-text);
}

.toggle-switch input:focus-visible + .toggle-switch-track {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

.toggle-switch-label {
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-medium);
  color: var(--mf-fg);
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  margin-bottom: var(--mf-space-4);
}

.form-group label {
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  font-weight: var(--mf-weight-medium);
}

.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea,
.form-group select {
  width: 100%;
  box-sizing: border-box;
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-3);
  color: var(--mf-fg);
  font-size: var(--mf-text-md);
  font-family: inherit;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.form-group input[type="text"],
.form-group input[type="password"],
.form-group select {
  height: var(--mf-control-height);
}

.form-group input::placeholder,
.form-group textarea::placeholder {
  color: var(--mf-fg-subtle);
}

.form-group input:hover,
.form-group textarea:hover,
.form-group select:hover {
  border-color: var(--mf-border-strong);
}

.form-group input:focus-visible,
.form-group textarea:focus-visible,
.form-group select:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.form-group input:disabled,
.form-group textarea:disabled,
.form-group select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* A field the server rejected. The border and the ring both change, because a
   colour change alone is not enough on its own to carry the meaning. */
.form-group.has-error input,
.form-group.has-error textarea,
.form-group.has-error select {
  border-color: var(--mf-danger-fg);
}

.form-group.has-error input:focus-visible,
.form-group.has-error textarea:focus-visible,
.form-group.has-error select:focus-visible {
  box-shadow: 0 0 0 3px var(--mf-danger-wash);
}

.form-error {
  font-size: var(--mf-text-xs);
  color: var(--mf-danger-fg);
  margin: 0;
}

.form-group textarea {
  min-height: 280px;
  padding: var(--mf-space-2) var(--mf-space-3);
  line-height: var(--mf-leading-normal);
  resize: vertical;
}

.form-hint {
  margin: 2px 0 0;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  line-height: var(--mf-leading-snug);
}

.form-hint code {
  background: var(--mf-surface-raised);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-xs);
  padding: 1px 5px;
  font-size: 0.92em;
  font-family: var(--mf-font-mono);
  color: var(--mf-accent-text);
}

.body-editor {
  position: relative;
  width: 100%;
}

.body-editor textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
}

.body-preview-box {
  width: 100%;
  box-sizing: border-box;
  min-height: 280px;
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-3);
  max-width: none;
  overflow-x: auto;
}

.body-preview-toggle {
  margin-top: var(--mf-space-2);
}

.form-check {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-direction: row;
}

.form-check input {
  width: 16px;
  height: 16px;
  accent-color: var(--mf-accent);
  flex-shrink: 0;
}

.form-actions {
  display: flex;
  gap: var(--mf-space-3);
  margin-top: var(--mf-space-2);
}

/* =============================================================================
   10. ADMIN ROWS
   ============================================================================= */

.admin-list {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
  margin-bottom: var(--mf-space-6);
}

.admin-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mf-space-3);
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  padding: var(--mf-space-3) var(--mf-space-4);
  transition: border-color var(--mf-dur-fast) var(--mf-ease);
}

.admin-row:hover {
  border-color: var(--mf-border-strong);
}

.admin-row-name {
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-md);
}

.admin-row-meta {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  font-variant-numeric: tabular-nums;
}

.admin-row-actions {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-shrink: 0;
}

.master-tag {
  display: inline-flex;
  align-items: center;
  background: var(--mf-gradient-brand);
  color: var(--mf-fg-on-accent);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  letter-spacing: var(--mf-tracking-wide);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  margin-left: var(--mf-space-2);
  text-transform: uppercase;
}

/* ---------------- Responsive ---------------- */
@media (max-width: 900px) {
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    z-index: var(--mf-z-drawer);
    transform: translateX(-100%);
    transition: transform var(--mf-dur-slow) var(--mf-ease-out);
    box-shadow: var(--mf-shadow-lg);
    /* Fixed + bottom:0 bounds the box but does not make it scrollable; the height rule
       here is what gives the overflow above something to work against on a drawer whose
       content is taller than the screen. */
    height: 100%;
    max-height: 100%;
    overflow-y: auto;
  }

  .sidebar.is-open {
    transform: translateX(0);
  }

  .hamburger {
    display: flex;
  }

  .sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: var(--mf-scrim);
    backdrop-filter: blur(2px);
    z-index: var(--mf-z-dropdown);
  }

  .sidebar-backdrop.is-open {
    display: block;
  }
}

@media (max-width: 640px) {
  .content {
    padding: var(--mf-space-4);
  }

  .topbar {
    padding: 0 var(--mf-space-4);
  }

  .post-row {
    flex-direction: column;
  }

  .post-row-actions {
    width: 100%;
  }

  .post-row-actions form,
  .post-row-actions a {
    flex: 1;
  }

  .post-row-actions .btn {
    width: 100%;
  }

  .admin-row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--mf-space-2);
  }

  .form-group textarea,
  .body-preview-box {
    min-height: 200px;
  }

  .form-actions {
    flex-direction: column;
  }

  .form-actions .btn {
    width: 100%;
  }
}

/* =============================================================================
   11. DOWNLOAD COUNTS
   Deliberately quiet. A download count is context, not a headline: it sits in the
   same grey meta row as the upload date and the file size, and only the module
   detail page — where somebody has actually gone looking for the number — gives it
   any weight. Big bold counters on every row would turn a management screen into a
   scoreboard, which changes what people optimise for.
   ============================================================================= */

.dl-count {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  color: var(--mf-fg-subtle);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.dl-count svg {
  opacity: 0.65;
  flex-shrink: 0;
}

.dl-card .dl-stats {
  display: flex;
  align-items: flex-end;
  gap: var(--mf-space-6);
  flex-wrap: wrap;
}

.dl-stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.dl-stat-value {
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tight);
  color: var(--mf-fg);
  font-variant-numeric: tabular-nums;
}

.dl-stat-label {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

/* The sparkline takes the leftover width so it stays readable on a wide screen and
   simply gets shorter on a narrow one, rather than forcing a horizontal scroll. */
.dl-spark {
  flex: 1 1 220px;
  min-width: 160px;
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  color: var(--mf-accent2-text);
}

.dl-spark .settings-hint {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-2xs);
}

@media (max-width: 640px) {
  .dl-card .dl-stats {
    gap: var(--mf-space-4);
  }

  .dl-stat-value {
    font-size: var(--mf-text-lg);
  }
}

/* =============================================================================
   12. CATALOG TOOLBAR (search / facets / sort)
   A GET form, so every control is a plain input and the resulting view is a URL.
   The layout wraps rather than scrolls: on a narrow screen the search box keeps
   its full width and the selects drop underneath, because a search field you have
   to scroll sideways to reach is a search field nobody uses.
   ============================================================================= */

.catalog-bar {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: var(--mf-space-3);
}

.catalog-search {
  flex: 1 1 260px;
  min-width: 190px;
  position: relative;
  display: flex;
  align-items: center;
}

.catalog-search svg {
  position: absolute;
  left: var(--mf-space-3);
  opacity: 0.5;
  pointer-events: none;
}

.catalog-search input {
  width: 100%;
  height: var(--mf-control-height);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-3) 0 var(--mf-space-6);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.catalog-search input::placeholder {
  color: var(--mf-fg-subtle);
}

.catalog-search input:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.catalog-bar select {
  height: var(--mf-control-height);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-2);
  font-family: inherit;
  font-size: var(--mf-text-sm);
  max-width: 100%;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.catalog-bar select:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

.catalog-chips {
  display: flex;
  gap: var(--mf-space-1);
  flex-wrap: wrap;
  margin-bottom: var(--mf-space-4);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
  height: var(--mf-control-height-sm);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg-muted);
  padding: 0 var(--mf-space-3);
  border-radius: var(--mf-radius-full);
  font-size: var(--mf-text-xs);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.chip:hover {
  border-color: var(--mf-border-strong);
  background: var(--mf-surface-hover);
  color: var(--mf-fg);
}

.chip.is-on {
  background: var(--mf-accent-wash);
  border-color: var(--mf-border-accent);
  color: var(--mf-accent-text);
}

/* A facet with no matches stays visible but recedes — it is still clickable, because
   "show me the empty set" is a legitimate way to confirm there is nothing there. */
.chip.is-empty {
  opacity: 0.42;
}

.chip-n {
  font-variant-numeric: tabular-nums;
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
}

.chip.is-on .chip-n {
  color: inherit;
}

/* Why a row is flagged, on the row. */
.attention-note {
  display: flex;
  align-items: center;
  gap: var(--mf-space-1);
  margin-top: var(--mf-space-2);
  font-size: var(--mf-text-xs);
  color: var(--mf-warn-fg);
}

.attention-note svg {
  flex-shrink: 0;
  opacity: 0.8;
}

/* =============================================================================
   13. THEME PREVIEWS
   A miniature of a MediaForge window: top bar, sidebar, content with a few lines
   and a call-to-action. Abstract on purpose — it has to read as "this is what the
   colours do" at 250px wide, and a faithful screenshot would be unreadable at that
   size while also being a much bigger promise than a palette can keep.
   ============================================================================= */

.tp {
  height: 132px;
  padding: var(--mf-space-2);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  border-radius: var(--mf-radius-md) var(--mf-radius-md) 0 0;
  overflow: hidden;
}

.tp-compact {
  height: 96px;
  padding: var(--mf-space-2);
  gap: var(--mf-space-1);
}

.tp-bar {
  height: 9px;
  border-radius: var(--mf-radius-xs);
  opacity: 0.9;
  flex-shrink: 0;
}

.tp-row {
  display: flex;
  gap: var(--mf-space-1);
  flex: 1;
  min-height: 0;
}

.tp-side {
  width: 26%;
  border-radius: var(--mf-radius-xs);
  opacity: 0.75;
}

.tp-main {
  flex: 1;
  border-radius: var(--mf-radius-xs);
  padding: var(--mf-space-2);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
}

.tp-line {
  height: 13px;
  border-radius: var(--mf-radius-xs);
  opacity: 0.85;
  flex-shrink: 0;
}

.tp-cta {
  margin-top: auto;
}

/* No palette. The hatch and the dashed edge are load-bearing: nobody should be able
   to mistake this for a preview of a theme that happens to be grey. */
.tp-none {
  background: var(--mf-surface-raised);
  background-image: repeating-linear-gradient(45deg,
    var(--mf-hatch) 0 8px, transparent 8px 16px);
  align-items: center;
  justify-content: center;
  gap: var(--mf-space-2);
}

.tp-none-mark {
  width: 44px;
  height: 44px;
  border-radius: var(--mf-radius-md);
  display: grid;
  place-items: center;
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border-strong);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-bold);
  font-size: var(--mf-text-md);
}

.tp-none-text {
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
}

.tp-facts {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin-top: var(--mf-space-2);
}

.tp-swatches {
  display: flex;
  gap: var(--mf-space-1);
}

.tp-sw {
  width: 15px;
  height: 15px;
  border-radius: var(--mf-radius-xs);
  border: 1px solid var(--mf-border-strong);
}

.tp-source {
  margin: var(--mf-space-2) 0 0;
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  line-height: var(--mf-leading-snug);
}

/* Gallery */
.tp-section {
  margin-bottom: var(--mf-space-6);
}

.tp-section-head {
  display: flex;
  align-items: baseline;
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-3);
}

.tp-section-head h2 {
  font-size: var(--mf-text-lg);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-tight);
  margin: 0;
}

.tp-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
  gap: var(--mf-space-3);
}

.tp-card {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  display: block;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.tp-card:hover {
  border-color: var(--mf-border-strong);
  box-shadow: var(--mf-shadow-md);
}

/* A theme with no preview is dashed all the way round, so the gallery reads at a
   glance as "these three have one, that one doesn't" without anyone counting. */
.tp-card.has-no-preview {
  border-style: dashed;
}

.tp-card-body {
  padding: var(--mf-space-3);
  border-top: 1px solid var(--mf-border);
}

.tp-card-title {
  margin: 0 0 var(--mf-space-1);
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

/* Detail page: preview beside its facts rather than above them, because there is
   room and the two are read together. */
.tp-detail {
  display: flex;
  gap: var(--mf-space-4);
  align-items: flex-start;
  flex-wrap: wrap;
}

.tp-detail .tp {
  width: 240px;
  flex: 0 0 auto;
  border-radius: var(--mf-radius-md);
}

.tp-detail-body {
  flex: 1 1 260px;
  min-width: 0;
}

@media (max-width: 640px) {
  .tp-detail .tp {
    width: 100%;
  }
}

/* =============================================================================
   14. MODULE LIST: headline figures, avatars, sparklines
   The list had grown into a wall of text: every row the same shape, the numbers
   that matter buried in a grey meta line, and a form above it all. These rules
   give a row three zones the eye can use — a coloured tile, the identity, the
   numbers — so a long catalog is scannable without being read.
   ============================================================================= */

.eyebrow {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
  color: var(--mf-accent-text);
  font-weight: var(--mf-weight-semibold);
  margin-bottom: var(--mf-space-1);
}

.page-header-actions {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  align-items: center;
}

.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-5);
}

.kpi {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  padding: var(--mf-space-3) var(--mf-space-4);
}

.kpi b {
  font-size: var(--mf-text-xl);
  font-variant-numeric: tabular-nums;
  display: block;
  line-height: var(--mf-leading-tight);
  letter-spacing: var(--mf-tracking-tight);
  font-weight: var(--mf-weight-semibold);
}

.kpi span {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  display: block;
  margin-top: 2px;
}

/* Only the counts that are non-zero get colour. A permanent amber "0 awaiting
   review" trains people to ignore amber, which is the one thing a warning colour
   cannot afford. */
.kpi.is-warn b { color: var(--mf-warn-fg); }
.kpi.is-bad b { color: var(--mf-danger-fg); }
.kpi span em { color: var(--mf-danger-fg); font-style: normal; font-weight: var(--mf-weight-semibold); }

.mod-avatar {
  width: 36px;
  height: 36px;
  border-radius: var(--mf-radius-sm);
  display: grid;
  place-items: center;
  font-weight: var(--mf-weight-bold);
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-on-accent);
  flex: 0 0 auto;
}

.mod-row-right {
  display: flex;
  align-items: center;
  gap: var(--mf-space-4);
}

.mod-spark {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 26px;
}

.mod-spark i {
  width: 3px;
  background: var(--mf-accent2);
  border-radius: 1px;
  display: block;
}

.mod-num {
  text-align: right;
  min-width: 74px;
}

.mod-num b {
  font-size: var(--mf-text-md);
  font-variant-numeric: tabular-nums;
  display: block;
  line-height: var(--mf-leading-tight);
  font-weight: var(--mf-weight-semibold);
}

.mod-num > span {
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  white-space: nowrap;
}

.mod-num em.is-up { color: var(--mf-ok-fg); font-style: normal; font-weight: var(--mf-weight-semibold); }
.mod-num em.is-down { color: var(--mf-danger-fg); font-style: normal; font-weight: var(--mf-weight-semibold); }

/* Collapsible cards: the upload form and the external-entry form. Both are things
   you want a couple of times a month, and both used to sit permanently open above
   the catalog they belong to. */
.upload-card > summary,
.info-alert.is-collapsible > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: var(--mf-space-2);
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-md);
}

.upload-card > summary::-webkit-details-marker,
.info-alert.is-collapsible > summary::-webkit-details-marker {
  display: none;
}

.upload-card > summary::before,
.info-alert.is-collapsible > summary::before {
  content: "›";
  display: inline-block;
  transition: transform var(--mf-dur-fast) var(--mf-ease);
  color: var(--mf-fg-subtle);
  font-size: 1.1em;
  line-height: 1;
}

.upload-card[open] > summary::before,
.info-alert.is-collapsible[open] > summary::before {
  transform: rotate(90deg);
}

.upload-card[open] > summary {
  margin-bottom: var(--mf-space-4);
}

.info-alert.is-collapsible > summary {
  font-size: var(--mf-text-sm);
}

.info-alert.is-collapsible[open] > summary {
  margin-bottom: var(--mf-space-2);
}

@media (max-width: 760px) {
  /* The numbers move under the identity rather than shrinking: a two-character
     download count is not worth keeping a third column alive for. */
  .mod-row-right {
    width: 100%;
    justify-content: space-between;
    gap: var(--mf-space-2);
  }

  .mod-spark { display: none; }
}

/* =============================================================================
   15. AUTOMATED REVIEW CHECKS AND REVIEW QUEUE
   A grid rather than a list: six findings read as a status panel you scan, where a
   list reads as prose you have to work through. The severity lives in the icon and
   a left rail, not in the background colour — a card that is 40% amber shouts at a
   reviewer who has already understood it.
   ============================================================================= */

.ck-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
  gap: var(--mf-space-2);
  margin: var(--mf-space-3) 0;
}

.ck-grid.is-compact {
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: var(--mf-space-1);
}

.ck {
  display: flex;
  gap: var(--mf-space-2);
  align-items: flex-start;
  background: var(--mf-bg-sunken);
  border-radius: var(--mf-radius-sm);
  border-left: 2px solid transparent;
  padding: var(--mf-space-2) var(--mf-space-3);
}

.ck-fail { border-left-color: var(--mf-danger-fg); }
.ck-warn { border-left-color: var(--mf-warn-fg); }
.ck-ok { border-left-color: color-mix(in srgb, var(--mf-ok-fg) 35%, transparent); }

.ck-ico {
  flex: 0 0 auto;
  font-weight: var(--mf-weight-bold);
  line-height: var(--mf-leading-snug);
  font-size: var(--mf-text-xs);
}

.ck-ico.is-ok { color: var(--mf-ok-fg); }
.ck-ico.is-warn { color: var(--mf-warn-fg); }
.ck-ico.is-fail { color: var(--mf-danger-fg); }
.ck-ico.is-skip { color: var(--mf-fg-subtle); }

.ck-body {
  min-width: 0;
}

.ck-body b {
  display: block;
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
}

.ck-detail {
  display: block;
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  line-height: var(--mf-leading-snug);
  margin-top: 1px;
}

.ck-items {
  margin: var(--mf-space-1) 0 0;
  padding-left: var(--mf-space-3);
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-muted);
}

.ck-items li {
  margin-bottom: 2px;
  word-break: break-word;
}

.ck-none {
  margin: var(--mf-space-2) 0 0;
}

.checks-fold {
  margin-top: var(--mf-space-2);
}

.checks-fold > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-muted);
}

.checks-fold > summary::-webkit-details-marker {
  display: none;
}

.checks-fold[open] > summary .chev svg {
  transform: rotate(90deg);
}

/* ---- Review queue ------------------------------------------------------- */
.review-card {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-left: 3px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-md);
  padding: var(--mf-space-4) var(--mf-space-5);
}

.review-card.is-flagged { border-left-color: var(--mf-warn-fg); }
.review-card.is-failing { border-left-color: var(--mf-danger-fg); }

.review-card .post-row-title a {
  color: inherit;
  text-decoration: none;
}

.review-card .post-row-title a:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.review-note {
  background: var(--mf-bg-sunken);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-2) var(--mf-space-3);
  margin-top: var(--mf-space-3);
  font-size: var(--mf-text-xs);
  line-height: var(--mf-leading-normal);
  color: var(--mf-fg-muted);
  white-space: pre-wrap;
}

.review-note-label {
  display: block;
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  margin-bottom: var(--mf-space-1);
  font-weight: var(--mf-weight-semibold);
}

/* =============================================================================
   16. AUDIT LOG
   A list, not a table: the entries are different shapes (some carry a reason, most
   don't) and a table would either lose the reason or leave a column empty on nine
   rows out of ten.
   ============================================================================= */

.audit-list {
  display: flex;
  flex-direction: column;
}

.audit-ev {
  padding: var(--mf-space-3) 0 var(--mf-space-3) var(--mf-space-4);
  border-left: 2px solid var(--mf-border);
  border-bottom: 1px solid var(--mf-border);
}

.audit-ev:last-child { border-bottom: none; }

/* Actions that change what the store vouches for, or what somebody can do. Everything
   else is bookkeeping and looks like it. */
.audit-ev.is-privileged { border-left-color: var(--mf-accent); }
.audit-ev.is-system { border-left-color: var(--mf-fg-subtle); opacity: 0.85; }

.audit-head {
  display: flex;
  align-items: baseline;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.audit-head b { font-size: var(--mf-text-sm); font-weight: var(--mf-weight-semibold); }

.audit-head time {
  margin-left: auto;
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  font-variant-numeric: tabular-nums;
}

.audit-target {
  margin: var(--mf-space-1) 0 0;
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-medium);
}

.audit-detail {
  margin: 2px 0 0;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-muted);
}

.audit-reason {
  border-left: 2px solid var(--mf-border-strong);
  padding-left: var(--mf-space-3);
  margin-top: var(--mf-space-2);
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  white-space: pre-wrap;
}

/* =============================================================================
   17. ROLE / TOKEN TABLES
   This is an admin tool, so density matters more than air. Uppercase micro
   headers in the subtle tone, hairline row borders, a real hover state, and
   tabular numerals so columns of figures line up on the decimal.
   ============================================================================= */

.role-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--mf-text-sm);
}

.role-table th {
  text-align: left;
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  padding: var(--mf-space-2) var(--mf-space-2);
  font-weight: var(--mf-weight-semibold);
  border-bottom: 1px solid var(--mf-border-strong);
  /* This header used to be `position: sticky; top: var(--mf-layout-topbar)`, on the
     reasoning that a long table scrolled past its own header is one you keep scrolling
     back up to read. That reasoning is fine; the implementation could not work here.

     Every .role-table in access.html sits inside a `.table-scroll`, which sets
     `overflow-x: auto`. An overflow container becomes the containing block for sticky
     descendants, so `top` stopped being measured from the viewport and started being
     measured from the top of that box — which parked the header 60px *down* into the
     table, painted over the first row of members. And because .table-scroll only ever
     scrolls horizontally, the header had nothing to stick to in the first place: all the
     rule could do was the damage.

     Making it work means the scroll container has to be the one with the height, which is
     a change to the markup and to how these tables are sized. Not worth it for tables that
     hold a handful of members. The background stays: it costs nothing and it is what the
     rule would need if this is ever revisited. */
  background: var(--mf-surface);
}

.role-table td {
  padding: var(--mf-space-2) var(--mf-space-2);
  border-top: 1px solid var(--mf-border);
  vertical-align: middle;
  font-variant-numeric: tabular-nums;
}

.role-table tbody tr {
  transition: background-color var(--mf-dur-fast) var(--mf-ease);
}

.role-table tbody tr:hover {
  background: var(--mf-surface-raised);
}

/* Numbers right-align so magnitudes are comparable down the column; the yes/no
   capability cells centre because they are marks, not quantities. */
.role-table td.is-num,
.role-table th.is-num {
  text-align: right;
}

.role-cell { text-align: center; }
.role-yes { color: var(--mf-ok-fg); font-weight: var(--mf-weight-bold); }
.role-no { color: var(--mf-fg-subtle); }

.role-table tr.is-revoked {
  opacity: 0.45;
}

/* Member row controls: store role + telemetry role + Save.
   Two selects instead of one since telemetry access became its own ladder (audit finding
   `Flaches admin_required ohne Abstufung`, see telemetry_roles.py). Right-aligned in a row
   on desktop; below ~720px the row would push the Save button off the edge of the scroll
   container, so it stacks and the controls go full width — a dropdown you have to scroll a
   table sideways to reach is a dropdown nobody changes. */
.role-form {
  display: flex;
  gap: var(--mf-space-2);
  justify-content: flex-end;
  align-items: center;
  flex-wrap: wrap;
}

@media (max-width: 720px) {
  .role-form {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    min-width: 11rem;
  }
  .role-form select,
  .role-form .btn {
    width: 100%;
  }
}

.role-table select {
  height: var(--mf-control-height-sm);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-2);
  font-family: inherit;
  font-size: var(--mf-text-xs);
}

/* ---- Channel controls --------------------------------------------------- */
.channel-group form {
  display: flex;
  gap: var(--mf-space-1);
  align-items: center;
}

.channel-select,
.rollout-input {
  height: var(--mf-control-height-sm);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-2);
  font-family: inherit;
  font-size: var(--mf-text-xs);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.rollout-input { width: 62px; font-variant-numeric: tabular-nums; }

.channel-select:focus-visible,
.rollout-input:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

/* Yank needs a reason, so it is a form rather than a button — laid out to fit the
   menu it lives in. */
.menu-yank {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  padding: var(--mf-space-2) var(--mf-space-3);
}

.menu-yank input[type="text"] {
  height: var(--mf-control-height-sm);
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-2);
  font-family: inherit;
  font-size: var(--mf-text-xs);
}

.menu-yank input[type="text"]:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

/* Why every grid above wraps its column floor in `min(..., 100%)` instead of stating the
   pixel value bare: a grid track will not shrink below its stated minimum, so on a 320px
   phone a 220px floor plus padding pushes the whole page sideways and produces a horizontal
   scrollbar on a layout that otherwise fits. Wrapping the floor in min(..., 100%) lets it
   collapse to the container width instead. It costs nothing on a wide screen — 100% is
   never the smaller value there. */

/* ---- Sidebar section headings -------------------------------------------
   Small, quiet, uppercase. They are there to chunk a menu that has grown from six
   entries to thirteen — at that length an undifferentiated list stops being scannable
   and people start hunting. The heading is not a control and must not read as one,
   hence no hover, no cursor change, and a colour closer to the background than to
   the links underneath it. */
.sidebar-section {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wider);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-subtle);
  padding: 0 var(--mf-space-3);
  margin: var(--mf-space-4) 0 var(--mf-space-1);
  /* An explicit line-height and min-height, so the box can never end up shorter than the
     text inside it. `overflow: hidden` is here for the ellipsis below, and a clipped
     heading is a far worse failure than a heading that wraps — this makes the clip
     impossible rather than merely unlikely. */
  line-height: 1.4;
  min-height: 14px;
  /* Long usernames are a real case (the last heading is the account name), and a
     heading that wraps to three lines looks like a bug. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The first heading sits directly under the brand, where the extra top margin would
   just be a gap. */
.sidebar-nav > .sidebar-section:first-child {
  margin-top: var(--mf-space-1);
}

/* =============================================================================
   18. RESPONSIVE CORRECTIONS
   The rule this section exists to enforce: nothing may make the *page* scroll
   sideways. A component that is too wide for a phone either wraps, or scrolls
   inside its own box — never both, and never neither.
   ============================================================================= */

/* Tables scroll inside themselves. The roles matrix is seven columns wide and
   there is no honest way to fit that on a 360px screen; the dishonest ways are
   shrinking the text to nothing, or letting it stretch the page so every other
   screen gains a horizontal scrollbar too. A scroll container keeps the damage
   local, and the shadow on the right edge is what tells you there is more. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* A right-edge shadow that appears only when there is something to scroll to.
     Pure CSS: the second layer sits in the scroll area and covers the first when
     the content is fully scrolled. */
  background:
    linear-gradient(to right, var(--mf-surface) 30%, transparent) left center,
    linear-gradient(to left, var(--mf-surface) 30%, transparent) right center,
    radial-gradient(farthest-side at 0 50%, var(--mf-edge-shade), transparent) left center,
    radial-gradient(farthest-side at 100% 50%, var(--mf-edge-shade), transparent) right center;
  background-repeat: no-repeat;
  background-size: 32px 100%, 32px 100%, 14px 100%, 14px 100%;
  background-attachment: local, local, scroll, scroll;
}

.table-scroll .role-table {
  min-width: 520px;
}

/* The narrow columns of the capability matrix should not be the ones that grow. */
.role-table .role-cell {
  white-space: nowrap;
}

/* A select inside a table row must not be the thing that forces the row wide. */
.role-table select {
  max-width: 160px;
}

/* =============================================================================
   ROW-AS-CARD

   `.table-scroll` is the house rule and it stays: a table that would be wider than
   the viewport scrolls sideways rather than pushing the whole document wide. But
   sideways scrolling is how a table *survives* a phone, not how it is *used* on
   one — you cannot compare two rows you can only ever see half of, and the column
   header is off-screen exactly when you need it.

   So below the breakpoint a table marked `.table-cards` stops being a grid and
   becomes a stack of cards, one per row, with each cell labelled by its own column
   header. The label comes from a `data-label` attribute on the <td>, which means
   the pattern is applied by adding an attribute to markup that already exists —
   not by writing a second, phone-shaped copy of every table inside an {% if %}
   that then has to be kept in step with the first one forever.

   Two things this deliberately does not do:

     * It does not replace `.table-scroll`. The wrapper stays on every table, both
       because tests/test_responsive.py requires it and because it is still what
       catches the tables that are *not* marked — the diff view and the
       rejected-events forensics table, where a card per row would be worse than a
       scrollbar (see the comments on those templates).
     * It does not hide the header row with `display: none`. The <thead> is
       clipped the same way `.sr-only` clips: a screen reader still announces the
       column names, and `display: none` would take them away from the one visitor
       who cannot see the `data-label` either.
   Every selector below is anchored on `.table-scroll`. That is not decoration:
   store.css and telemetry.css are loaded *after* dashboard.css, so a bare
   `.table-cards td` would lose to their `.telemetry-table td` at identical
   specificity, purely on load order — and it would lose silently, on a phone,
   which is the one place nobody is looking. The wrapper is guaranteed to be there
   by the house rule test_responsive.py enforces, so leaning on it costs nothing.
   ============================================================================= */

@media (max-width: 640px) {
  .table-scroll .table-cards thead {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .table-scroll .table-cards,
  .table-scroll .table-cards tbody,
  .table-scroll .table-cards tr,
  .table-scroll .table-cards td {
    display: block;
    width: 100%;
  }

  .table-scroll .table-cards tr {
    border: 1px solid var(--mf-border);
    border-radius: var(--mf-radius-md);
    background: var(--mf-surface-raised);
    padding: var(--mf-space-2) var(--mf-space-3);
    margin-bottom: var(--mf-space-3);
  }

  .table-scroll .table-cards tr:last-child {
    margin-bottom: 0;
  }

  /* The row hover state was a way of tracking one row across a wide table. A card
     is already one row, so the highlight has nothing left to say. */
  .table-scroll .table-cards tbody tr:hover {
    background: var(--mf-surface-raised);
  }

  .table-scroll .table-cards td {
    display: flex;
    align-items: baseline;
    gap: var(--mf-space-3);
    padding: var(--mf-space-1) 0;
    border: none;
    border-bottom: 1px solid var(--mf-border);
    text-align: left;
  }

  .table-scroll .table-cards tr td:last-child {
    border-bottom: none;
  }

  /* Flex, not a two-column grid. A grid track floor that cannot shrink is the
     failure the grid rule in docs/DESIGN.md exists to stop, and a flex basis with
     `flex-shrink` avoids the question entirely: a long label gives way instead of
     forcing the value into a two-character column. */
  .table-scroll .table-cards td[data-label]::before {
    content: attr(data-label);
    flex: 0 1 38%;
    min-width: 0;
    color: var(--mf-fg-subtle);
    font-size: var(--mf-text-2xs);
    font-weight: var(--mf-weight-semibold);
    text-transform: uppercase;
    letter-spacing: var(--mf-tracking-wide);
    overflow-wrap: anywhere;
  }

  /* The value side. min-width:0 because a flex item defaults to min-width:auto,
     which is what lets one long hash push the card wider than the screen — the
     exact failure the card layout exists to avoid. */
  .table-scroll .table-cards td > * {
    min-width: 0;
  }

  /* A cell with no data-label is a full-width row inside the card: an actions
     bar, a nested list, a wide code block. Those want the whole width, not 62% of
     it next to an empty label column. */
  .table-scroll .table-cards td:not([data-label]) {
    display: block;
  }

  /* Numbers stop being right-aligned once they are no longer in a column. There is
     nothing to line them up with, and a lone right-aligned figure next to its own
     left-aligned label just reads as a mistake. */
  .table-scroll .table-cards td.is-num,
  .table-scroll .table-cards td.cc-num,
  .table-scroll .table-cards td.num,
  .table-scroll .table-cards .role-cell {
    text-align: left;
  }
}

/* Two spans side by side in a summary — the hint is the one that should give way. */
.upload-summary {
  flex-wrap: wrap;
}

.upload-summary .settings-hint {
  flex: 1 1 auto;
  min-width: 0;
}

/* Long ids, hashes and hostnames are the classic source of a page that scrolls
   sideways on a phone. They are allowed to break mid-token; everything else is
   not, because breaking prose mid-word is worse than a slightly ragged edge. */
.ck-items code,
.audit-target,
.audit-detail,
.post-row-meta code,
.tp-card-body code {
  overflow-wrap: anywhere;
}

@media (max-width: 640px) {
  /* The KPI strip goes two-up rather than one-up: five stacked cards push the
     content people came for below the fold, and these are glanceable numbers,
     not reading matter. */
  .kpi-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--mf-space-2);
  }

  .kpi {
    padding: var(--mf-space-3);
  }

  .kpi b {
    font-size: var(--mf-text-lg);
  }

  /* The last card in an odd-numbered row spans the full width instead of leaving
     a hole next to it. */
  .kpi-row .kpi:last-child:nth-child(odd) {
    grid-column: 1 / -1;
  }

  /* Facet chips: a horizontal strip beats four ragged rows of pills, and swiping
     a chip bar is a familiar gesture. */
  .catalog-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    padding-bottom: var(--mf-space-1);
    scrollbar-width: none;
  }

  .catalog-chips::-webkit-scrollbar {
    display: none;
  }

  .chip {
    flex: 0 0 auto;
  }

  /* The search field keeps the full width; the two selects share the next line. */
  .catalog-search {
    flex: 1 1 100%;
  }

  .catalog-bar select {
    flex: 1 1 calc(50% - var(--mf-space-1));
    min-width: 0;
  }

  /* One check per row. Two 190px columns inside a card inside 16px of page
     padding is exactly the arithmetic that produces a sideways scroll. */
  .ck-grid,
  .ck-grid.is-compact {
    grid-template-columns: 1fr;
  }

  /* NOTE: the release action bar already stacks at this width — those rules live in
     store.css, which is loaded after this file and would win anyway. Duplicating them
     here would be two places to edit and one place to forget. The store-specific
     additions (channel controls, decline field) are in store.css for the same reason. */

  .audit-head time {
    margin-left: 0;
    width: 100%;
  }

  .role-table select {
    max-width: 130px;
  }
}

@media (max-width: 400px) {
  /* Below this the two-up KPI strip stops being readable — the numbers start
     wrapping inside their own cards, which looks broken rather than dense. */
  .kpi-row {
    grid-template-columns: 1fr;
  }

  .tp-gallery {
    gap: var(--mf-space-2);
  }
}

/* ---- Short viewports ----------------------------------------------------
   The full menu is around 920px tall for an owner, so it overflows a laptop window
   and a landscape phone, not just a small screen. Scrolling handles that correctly,
   but scrolling a menu is still worse than not having to — so on a short viewport
   the padding comes down and most of the height goes away.

   Keyed on height, not width: a 1440x700 laptop window has the same problem as a
   phone on its side, and a width breakpoint would miss both. */
@media (max-height: 760px) {
  .sidebar {
    padding: var(--mf-space-4) var(--mf-space-3);
    gap: var(--mf-space-3);
  }

  .sidebar-link {
    padding: var(--mf-space-1) var(--mf-space-3);
  }

  .sidebar-section {
    margin: var(--mf-space-3) 0 var(--mf-space-1);
  }

  .sidebar-footer {
    display: none;
  }
}

@media (max-height: 560px) {
  /* Landscape phones. The headings are the first thing to go: at this height they
     cost more rows than they save hunting, and the icons still group the entries
     visually. */
  .sidebar-section {
    display: none;
  }

  .sidebar-link {
    padding: var(--mf-space-1) var(--mf-space-3);
    font-size: var(--mf-text-xs);
  }
}

/* =============================================================================
   19. AUDIT FINDINGS BOARD
   Severity colours are local to this page on purpose: they express "how bad is
   this defect", which is a different axis from the badge-* tokens above (those
   classify content type). They come from the --sev-* tokens, which tokens.css
   now maps onto the same status ramp everything else uses.
   ============================================================================= */

.sidebar-badge-danger {
  background: var(--mf-danger-wash);
  color: var(--mf-danger-fg);
}

/* --- scorecard --- */
.finding-scores {
  display: grid;
  /* min() so the track can collapse below its preferred floor on a narrow
     screen. A floor stated bare cannot shrink past itself, so on a viewport
     narrower than the floor the grid overflows the page instead of reflowing —
     which is the whole failure mode test_responsive.py guards against. This was
     the last rigid minimum left in the file. */
  grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
  gap: var(--mf-space-2);
  margin: var(--mf-space-5) 0;
}

.finding-score {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  padding: var(--mf-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
}

.finding-score-label {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
}

.finding-score-value {
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-bold);
  color: var(--mf-fg);
  line-height: 1;
  letter-spacing: var(--mf-tracking-tight);
  font-variant-numeric: tabular-nums;
}

.finding-score.sev-crit .finding-score-value { color: var(--sev-crit); }
.finding-score.sev-high .finding-score-value { color: var(--sev-high); }
.finding-score.sev-med  .finding-score-value { color: var(--sev-med); }
.finding-score.sev-low  .finding-score-value { color: var(--sev-low); }

/* --- cards --- */
.finding-card {
  background: var(--mf-surface);
  border: 1px solid var(--mf-border);
  border-left: 3px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-md);
  margin-bottom: var(--mf-space-2);
  overflow: hidden;
}

.finding-card.sev-crit { border-left-color: var(--sev-crit); }
.finding-card.sev-high { border-left-color: var(--sev-high); }
.finding-card.sev-med  { border-left-color: var(--sev-med); }
.finding-card.sev-low  { border-left-color: var(--sev-low); }

.finding-summary {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-3) var(--mf-space-4);
  cursor: pointer;
  list-style: none;
  user-select: none;
  transition: background-color var(--mf-dur-fast) var(--mf-ease);
}

.finding-summary::-webkit-details-marker { display: none; }
.finding-summary:hover { background: var(--mf-surface-hover); }

.finding-sev {
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  flex-shrink: 0;
  background: var(--mf-surface-raised);
  color: var(--mf-fg-muted);
}

.sev-crit > .finding-summary .finding-sev { background: var(--mf-danger-wash); color: var(--sev-crit); }
.sev-high > .finding-summary .finding-sev { background: var(--mf-warn-wash); color: var(--sev-high); }
.sev-med  > .finding-summary .finding-sev { background: var(--mf-warn-wash); color: var(--sev-med); }
.sev-low  > .finding-summary .finding-sev { background: var(--mf-ok-wash); color: var(--sev-low); }

.finding-title {
  flex: 1;
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-sm);
  color: var(--mf-fg);
  min-width: 0;
}

/* A fixed finding stays legible but visibly settled — it is history, not backlog. */
.finding-card.status-fixed .finding-title {
  color: var(--mf-fg-muted);
  text-decoration: line-through;
  text-decoration-color: var(--mf-fg-subtle);
}

.finding-area {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  flex-shrink: 0;
}

.badge-assignee {
  background: var(--mf-accent-wash);
  color: var(--mf-accent-text);
}

.badge-status-open  { background: var(--badge-bg); color: var(--mf-fg-muted); }
.badge-status-wip   { background: var(--mf-warn-wash); color: var(--mf-warn-fg); }
.badge-status-fixed { background: var(--mf-ok-wash); color: var(--mf-ok-fg); }

/* --- body --- */
.finding-body {
  padding: var(--mf-space-1) var(--mf-space-4) var(--mf-space-4);
  border-top: 1px solid var(--mf-border);
}

.finding-body h4 {
  margin: var(--mf-space-4) 0 var(--mf-space-1);
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
}

.finding-body p {
  margin: 0 0 var(--mf-space-2);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

.finding-refs code {
  display: inline-block;
  margin: 0 var(--mf-space-1) var(--mf-space-1) 0;
  font-size: var(--mf-text-xs);
  font-family: var(--mf-font-mono);
}

/* The remedy is the one block on this page allowed a wash background: it is the
   answer, and it should be findable without reading the description first.
   `!important` on the colour because .finding-body p above would otherwise win
   on specificity for the paragraph this class lands on. */
.finding-remedy {
  background: var(--mf-ok-wash);
  border: 1px solid color-mix(in srgb, var(--mf-ok-fg) 25%, transparent);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-2) var(--mf-space-3);
  color: var(--mf-ok-fg) !important;
}

.finding-fixline {
  font-size: var(--mf-text-xs);
  color: var(--mf-ok-fg) !important;
  padding-top: var(--mf-space-1);
}

.finding-note {
  font-size: var(--mf-text-sm);
  border-left: 2px solid var(--mf-border-strong);
  padding-left: var(--mf-space-3);
}

.finding-key {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  text-transform: none;
  letter-spacing: var(--mf-tracking-normal);
  font-weight: var(--mf-weight-normal);
  margin-left: var(--mf-space-1);
}

/* --- actions --- */
.finding-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
  align-items: center;
  background: var(--mf-bg-sunken);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-3);
}

.finding-form {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.finding-form-fix {
  flex: 1;
  min-width: 320px;
  border-left: 1px solid var(--mf-border);
  padding-left: var(--mf-space-3);
}

.finding-form label {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
}

.finding-form input[type="text"],
.finding-form select {
  height: var(--mf-control-height-sm);
  background: var(--mf-bg);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg);
  border-radius: var(--mf-radius-sm);
  padding: 0 var(--mf-space-2);
  font-family: inherit;
  font-size: var(--mf-text-xs);
  min-width: 110px;
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              box-shadow var(--mf-dur-fast) var(--mf-ease);
}

.finding-form input[type="text"]:focus-visible,
.finding-form select:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: 0 0 0 3px var(--mf-accent-wash);
}

/* --- retired --- */
.finding-retired {
  margin-top: var(--mf-space-6);
  border-top: 1px solid var(--mf-border);
  padding-top: var(--mf-space-4);
}

.finding-retired > summary {
  cursor: pointer;
  color: var(--mf-fg-muted);
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-sm);
}

.finding-retired-note {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
  margin: var(--mf-space-2) 0;
}

@media (max-width: 720px) {
  .finding-summary { flex-wrap: wrap; }
  .finding-title { flex-basis: 100%; order: -1; }
  .finding-form-fix { border-left: 0; padding-left: 0; min-width: 0; }
}

.finding-scope {
  background: var(--mf-accent-wash);
  border: 1px solid var(--mf-border-accent);
  border-radius: var(--mf-radius-sm);
  padding: var(--mf-space-2) var(--mf-space-3);
  margin: var(--mf-space-2) 0;
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
}

.finding-scope strong { color: var(--mf-accent-text); }

.finding-scores-caption {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  margin: var(--mf-space-4) 0 calc(var(--mf-space-2) * -1);
}

/* --- project tabs (Dev Server / MediaForge App) --- */
.finding-tabs {
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--mf-border);
  margin: var(--mf-space-5) 0 0;
  overflow-x: auto;
}

.finding-tab {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-3) var(--mf-space-4);
  border-bottom: 2px solid transparent;
  color: var(--mf-fg-subtle);
  text-decoration: none;
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-sm);
  white-space: nowrap;
  transition: color var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease);
}

.finding-tab:hover { color: var(--mf-fg); }

.finding-tab.active {
  color: var(--mf-fg);
  border-bottom-color: var(--mf-accent);
}

.finding-tab-count {
  background: var(--badge-bg);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-semibold);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  font-variant-numeric: tabular-nums;
}

.finding-tab-crit {
  background: var(--mf-danger-wash);
  color: var(--mf-danger-fg);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  font-variant-numeric: tabular-nums;
}

/* =============================================================================
   Linked accounts (account page)

   A neutral row, not a feature card: a linked identity is a fact about the
   account, and the only action on it (Unlink) is deliberately quiet. The GitHub
   button is a *secondary* action here for the same reason the sign-in page keeps
   it below the password form — connecting an account is optional, and an optional
   action should never out-shout the primary one.
   ============================================================================= */

.linked-account {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  padding: var(--mf-space-3);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-sm);
  background: var(--mf-surface-raised);
}

.linked-account-avatar {
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  border-radius: var(--mf-radius-full);
  border: 1px solid var(--mf-border-strong);
  object-fit: cover;
  background: var(--mf-surface);
}

.linked-account-body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mf-space-2);
  min-width: 0;
  flex: 1 1 auto;
}

.linked-account-body strong {
  font-size: var(--mf-text-md);
  color: var(--mf-fg);
  overflow-wrap: anywhere;
}

.linked-account-meta {
  flex-basis: 100%;
  font-size: var(--mf-text-xs);
}

/* The GitHub mark inherits the button's text colour, so there is one thing to
   theme rather than two. */
.btn-github-mark {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
}

.btn-github {
  background: var(--mf-surface-raised);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg);
}

.btn-github:hover {
  background: var(--mf-surface-hover);
  border-color: var(--mf-border-accent);
}

.btn-github:active {
  background: var(--mf-surface-active);
}

@media (max-width: 560px) {
  .linked-account {
    flex-wrap: wrap;
  }
}

/* ===========================================================================
   Release pipeline (templates/dashboard/_governance.html)

   Drawn as an ordered vertical list with a connecting rail, because the steps
   are a sequence and a grid of tiles would say they are not. Status colour is
   carried by the icon and a thin left edge only: a fully colour-washed row for
   every step turns a page with six steps into a traffic light nobody reads.
   =========================================================================== */

.gv-pipeline {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
}

.gv-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.gv-head-label {
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-wide);
  text-transform: uppercase;
  color: var(--mf-fg-subtle);
}

.gv-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
}

.gv-step {
  position: relative;
  display: flex;
  gap: var(--mf-space-3);
  padding: var(--mf-space-3);
  border-radius: var(--mf-radius-sm);
  background: var(--mf-surface-raised);
  border-left: 2px solid var(--mf-border-strong);
}

/* The rail: a hairline joining one step's icon to the next, so the list reads
   as a pipeline rather than as six unrelated cards. */
.gv-step::after {
  content: "";
  position: absolute;
  left: calc(var(--mf-space-3) + 9px);
  top: calc(var(--mf-space-3) + 22px);
  bottom: calc(var(--mf-space-1) * -1);
  width: 1px;
  background: var(--mf-border-strong);
}

.gv-step:last-child::after { display: none; }

.gv-step.is-passed  { border-left-color: var(--mf-ok-fg); }
.gv-step.is-failed  { border-left-color: var(--mf-danger-fg); background: var(--mf-danger-wash); }
.gv-step.is-waived  { border-left-color: var(--mf-warn-fg); background: var(--mf-warn-wash); }
.gv-step.is-skipped { border-left-color: var(--mf-border-strong); }
.gv-step.is-pending { border-left-color: var(--mf-border); }
.gv-step.is-retired { opacity: 0.65; }

.gv-ico {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: var(--mf-radius-full);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-bold);
  line-height: 1;
  position: relative;
  z-index: var(--mf-z-base);
  background: var(--mf-surface-active);
  color: var(--mf-fg-muted);
}

.gv-ico.is-passed  { background: var(--mf-ok-wash);     color: var(--mf-ok-fg); }
.gv-ico.is-failed  { background: var(--mf-danger-wash); color: var(--mf-danger-fg); }
.gv-ico.is-waived  { background: var(--mf-warn-wash);   color: var(--mf-warn-fg); }
.gv-ico.is-skipped { background: var(--mf-surface-active); color: var(--mf-fg-subtle); }
.gv-ico.is-pending { background: var(--mf-surface-active); color: var(--mf-fg-subtle); }

.gv-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
}

.gv-label {
  margin: 0;
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg);
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.gv-status {
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-medium);
  letter-spacing: var(--mf-tracking-wide);
  text-transform: uppercase;
  color: var(--mf-fg-subtle);
}

.gv-step.is-passed .gv-status { color: var(--mf-ok-fg); }
.gv-step.is-failed .gv-status { color: var(--mf-danger-fg); }
.gv-step.is-waived .gv-status { color: var(--mf-warn-fg); }

.gv-tag {
  font-size: var(--mf-text-2xs);
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-full);
  border: 1px solid var(--mf-border-strong);
  color: var(--mf-fg-subtle);
}

.gv-tag.is-blocking {
  border-color: var(--mf-danger-fg);
  color: var(--mf-danger-fg);
}

.gv-detail {
  margin: 0;
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  overflow-wrap: anywhere;
}

/* A waiver is a person overriding a gate. It is drawn as its own statement so
   that "cleared" and "cleared over an objection" can never read the same. */
.gv-waiver {
  margin: 0;
  font-size: var(--mf-text-sm);
  color: var(--mf-warn-fg);
  overflow-wrap: anywhere;
}

.gv-meta {
  margin: 0;
  display: flex;
  gap: var(--mf-space-3);
  flex-wrap: wrap;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

.gv-decide > summary {
  cursor: pointer;
  font-size: var(--mf-text-xs);
  color: var(--mf-accent-text);
  margin-top: var(--mf-space-1);
}

.gv-decide-form {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  align-items: center;
  margin-top: var(--mf-space-2);
}

.gv-decide-form input[type="text"] { flex: 1 1 200px; min-width: 0; }

.gv-fold > summary {
  cursor: pointer;
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  padding: var(--mf-space-2) 0;
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
}

/* ===========================================================================
   Source repository card (templates/dashboard/module_detail.html)
   =========================================================================== */

.repo-card {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
}

.repo-card-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.repo-card-head h3 {
  margin: 0;
  font-size: var(--mf-text-base);
  color: var(--mf-fg);
}

.repo-linked {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-2);
}

.repo-name {
  margin: 0;
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  overflow-wrap: anywhere;
}

.repo-meta {
  margin: 0;
  display: flex;
  gap: var(--mf-space-4);
  flex-wrap: wrap;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

.repo-note { margin: 0; }

.repo-import,
.repo-link-fields {
  display: flex;
  gap: var(--mf-space-2);
  align-items: center;
  flex-wrap: wrap;
}

.repo-import label {
  flex-basis: 100%;
  font-size: var(--mf-text-xs);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-wide);
  text-transform: uppercase;
  color: var(--mf-fg-subtle);
}

.repo-import select { flex: 1 1 260px; min-width: 0; }
.repo-link-fields input[type="text"] { flex: 1 1 240px; min-width: 0; }

.repo-link-form {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
}

.repo-unlink { margin-top: var(--mf-space-1); }

@media (max-width: 560px) {
  .repo-import select,
  .repo-link-fields input[type="text"] { flex-basis: 100%; }
}

/* =============================================================================
   COMMAND CENTER (dashboard/overview.html)
   =============================================================================
   The overview answers one question — does anything need a decision right now —
   and the type scale is what says so: the pulse figures and the queue titles are
   the only things above the small-text tier. Store and member counters moved to
   the bottom and are drawn at the same weight as everything else there, which is
   the point: they are figures, not decisions.

   Written narrow-first. Every grid here is a single column until it has room,
   because a phone is where an admin actually reads "is anything on fire", and
   every column floor is wrapped in min(..., 100%) so it can collapse instead of
   pushing the page sideways.
   ============================================================================= */

/* ---- the shared "not measured" mark --------------------------------------
   A value nobody measured renders as this, never as 0. It is deliberately quiet
   rather than red: the *state* rows carry the alarm, this is only the absence of
   a number. */
.mf-unknown {
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-normal);
}

.tone-ok      { color: var(--mf-ok-fg); }
.tone-warn    { color: var(--mf-warn-fg); }
.tone-danger  { color: var(--mf-danger-fg); }
.tone-info    { color: var(--mf-info-fg); }
.tone-muted,
.tone-unknown { color: var(--mf-fg-subtle); }

/* ---- message thread ------------------------------------------------------
   A transcript, not a chat app: every message is the same full-width block, in
   order, and the only difference between the two sides is which edge is marked.
   Alternating left/right bubbles look like a phone and read like one — which is
   wrong for something people write paragraphs into. */
.msg {
  padding: var(--mf-space-4) 0;
  border-bottom: 1px solid var(--mf-border);
  border-left: 3px solid transparent;
  padding-left: var(--mf-space-4);
}

.msg:last-child { border-bottom: none; }
.msg-admin { border-left-color: var(--mf-accent); }

.msg-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  margin-bottom: var(--mf-space-2);
  flex-wrap: wrap;
}

/* The message itself is plain text with its line breaks kept — see the template
   for why it is never rendered as markup. */
.msg-body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--mf-fg);
  line-height: 1.6;
}

/* ---- pulse strip ---------------------------------------------------------
   One-pixel gaps over the border colour rather than per-cell borders: the strip
   reads as one instrument, and there is no double line where two cells meet. */
.pulse-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr));
  gap: 1px;
  background: var(--mf-border);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  overflow: hidden;
  margin-bottom: var(--mf-space-5);
}

.pulse-cell {
  background: var(--mf-surface);
  padding: var(--mf-space-4);
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  min-width: 0;
}

/* A cell that links to the panel it counts. It keeps the strip's typography — an
   underlined blue figure would read as a different kind of thing — and says it is
   followable with a cursor, a hairline arrow and a lift on hover. */
a.pulse-cell {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  position: relative;
  transition: background-color var(--mf-dur-fast) var(--mf-ease);
}

a.pulse-cell::after {
  content: "→";
  position: absolute;
  top: var(--mf-space-4);
  right: var(--mf-space-4);
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-subtle);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity var(--mf-dur-fast) var(--mf-ease),
              transform var(--mf-dur-fast) var(--mf-ease);
}

a.pulse-cell:hover::after,
a.pulse-cell:focus-visible::after {
  opacity: 1;
  transform: translateX(0);
}

a.pulse-cell:hover {
  background-color: var(--mf-surface-raised);
}

/* The queue panel is what the linked cell jumps to. Without this the heading lands
   under the sticky topbar and the reader sees the middle of the list. */
#queue {
  scroll-margin-top: var(--mf-space-6);
}

/* Two washes, and only two. A cell that needs a human and a cell that is
   confirmed clean are the two states worth colouring the background for; every
   other distinction is carried by the figure's own colour. */
.pulse-alert { background: linear-gradient(180deg, var(--mf-danger-wash), transparent); }
.pulse-good  { background: linear-gradient(180deg, var(--mf-ok-wash), transparent); }

.pulse-label {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
}

.pulse-value {
  font-size: var(--mf-text-2xl);
  font-weight: var(--mf-weight-bold);
  line-height: 1.1;
  /* These change in place on every reload; tabular figures stop them jittering. */
  font-variant-numeric: tabular-nums;
}

.cc-unit {
  font-size: var(--mf-text-lg);
  color: var(--mf-fg-subtle);
  margin-left: 1px;
}

.pulse-detail {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  overflow-wrap: anywhere;
}

.pulse-spark {
  display: block;
  width: 100%;
  height: 24px;
  color: var(--mf-accent-text);
  margin-top: var(--mf-space-1);
}

/* ---- the card grid -------------------------------------------------------- */
.cc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: var(--mf-space-4);
  align-items: start;
}

.cc-card {
  padding: var(--mf-space-4);
  min-width: 0;
}

/* The queue is the only panel on this screen anybody has to act on, so it gets
   the full width wherever there is more than one column to take. */
@media (min-width: 900px) {
  .cc-card-wide { grid-column: 1 / -1; }
}

.cc-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--mf-space-3);
  flex-wrap: wrap;
  margin-bottom: var(--mf-space-4);
}

.cc-card-head h2 {
  font-size: var(--mf-text-lg);
  margin: 0;
}

.cc-note {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  margin: var(--mf-space-3) 0 0;
  line-height: 1.6;
}

.cc-eyebrow {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
  margin-bottom: var(--mf-space-1);
}

.cc-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
  gap: var(--mf-space-4);
  margin-bottom: var(--mf-space-4);
}

.cc-figure {
  font-size: var(--mf-text-2xl);
  font-weight: var(--mf-weight-bold);
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  overflow-wrap: anywhere;
}

.cc-sub {
  margin-top: var(--mf-space-4);
  padding-top: var(--mf-space-4);
  border-top: 1px solid var(--mf-border);
}

.cc-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
}

.cc-mono {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  overflow-wrap: anywhere;
}

/* ---- queue items ---------------------------------------------------------
   Severity is a bar rather than a coloured background: the row still has to be
   readable, and three tinted backgrounds in a stack turn a work list into a
   traffic light nobody can scan. */
.queue-item {
  display: flex;
  gap: var(--mf-space-3);
  align-items: flex-start;
  flex-wrap: wrap;
  padding: var(--mf-space-3);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  background: var(--mf-surface-raised);
  transition: border-color var(--mf-dur-fast) var(--mf-ease),
              background-color var(--mf-dur-fast) var(--mf-ease);
}

.queue-item:hover {
  border-color: var(--mf-border-strong);
  background: var(--mf-surface-hover);
}

.queue-item + .queue-item { margin-top: var(--mf-space-2); }

.q-sev {
  width: 3px;
  border-radius: var(--mf-radius-xs);
  align-self: stretch;
  flex: none;
  background: var(--mf-fg-subtle);
}

.q-sev-danger { background: var(--mf-danger-fg); }
.q-sev-warn   { background: var(--mf-warn-fg); }
.q-sev-info   { background: var(--mf-info-fg); }

.q-body {
  flex: 1 1 220px;
  min-width: 0;
}

.q-title {
  font-weight: var(--mf-weight-semibold);
  font-size: var(--mf-text-sm);
  overflow-wrap: anywhere;
}

.q-sub {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  margin-top: 2px;
  overflow-wrap: anywhere;
}

/* The buttons wrap under the text on a phone rather than squeezing it — an
   action you cannot read the subject of is not an action. */
.q-act {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  flex: none;
}

.q-act form { margin: 0; }

/* ---- tables --------------------------------------------------------------- */
.cc-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--mf-text-sm);
}

.cc-table th {
  text-align: left;
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-subtle);
  font-weight: var(--mf-weight-semibold);
  padding: 0 var(--mf-space-3) var(--mf-space-2) 0;
  border-bottom: 1px solid var(--mf-border);
  white-space: nowrap;
}

.cc-table td {
  padding: var(--mf-space-2) var(--mf-space-3) var(--mf-space-2) 0;
  border-bottom: 1px solid var(--mf-border);
  vertical-align: top;
}

.cc-table tr:last-child td { border-bottom: none; }

.cc-num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--mf-fg-muted);
}

.cc-signal {
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
}

/* ---- state badges --------------------------------------------------------
   One class per state, chosen by the route, so a template cannot invent a
   seventh state. `unknown` is red on purpose: a subsystem nobody measures is a
   subsystem nobody fixes, and a grey row is one people read as "fine". */
.badge-state {
  text-transform: none;
  letter-spacing: 0;
  font-weight: var(--mf-weight-semibold);
}

.badge-ok      { background: var(--mf-ok-wash);     color: var(--mf-ok-fg); }
.badge-warn    { background: var(--mf-warn-wash);   color: var(--mf-warn-fg); }
.badge-danger  { background: var(--mf-danger-wash); color: var(--mf-danger-fg); }
.badge-info    { background: var(--mf-info-wash);   color: var(--mf-info-fg); }
.badge-unknown {
  background: var(--mf-danger-wash);
  color: var(--mf-danger-fg);
  box-shadow: inset 0 0 0 1px var(--mf-danger-wash);
}
.badge-muted,
.badge-empty   { background: var(--mf-bg-sunken);   color: var(--mf-fg-subtle); }

/* ---- audit trail ---------------------------------------------------------- */
.cc-trail {
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--mf-space-5);
  position: relative;
}

.cc-trail::before {
  content: "";
  position: absolute;
  left: 5px;
  top: var(--mf-space-2);
  bottom: var(--mf-space-2);
  width: 1px;
  background: var(--mf-border-strong);
}

.cc-trail-item {
  position: relative;
  padding-bottom: var(--mf-space-3);
}

.cc-trail-item:last-child { padding-bottom: 0; }

.cc-trail-dot {
  position: absolute;
  left: calc(-1 * var(--mf-space-5) + 1px);
  top: 5px;
  width: 11px;
  height: 11px;
  border-radius: var(--mf-radius-full);
  border: 2px solid var(--mf-bg);
  background: var(--mf-fg-subtle);
}

/* Signing, role changes, key management: the actions that change what the store
   vouches for or what somebody may do. Colouring them the same as bookkeeping
   would hide the only distinction this list carries. */
.cc-trail-dot-priv { background: var(--mf-warn-fg); }

.cc-trail-when {
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  font-family: var(--mf-font-mono);
}

.cc-trail-what {
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  margin-top: 2px;
  overflow-wrap: anywhere;
}

.cc-trail-reason {
  display: block;
  color: var(--mf-fg-subtle);
  font-size: var(--mf-text-xs);
}

/* ---- role picker (dashboard/admins.html) ---------------------------------- */
.role-picker {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  align-items: center;
  margin: 0;
}

.role-picker select {
  min-width: 0;
  flex: 1 1 160px;
}

@media (max-width: 480px) {
  /* Below this the queue's buttons and the card padding are competing for the
     same 40 px. The buttons win — they are the reason the row exists. */
  .cc-card { padding: var(--mf-space-3); }
  .q-act { width: 100%; }
  .q-act .btn { flex: 1 1 auto; justify-content: center; }
}

/* =============================================================================
   SETTINGS SURFACE (dashboard/settings.html and its four aliases)
   =============================================================================
   Five pages became one screen with five sections. The section nav is a list of
   links, not a tab widget, because these are five URLs and behaving like links
   is what makes them bookmarkable, middle-clickable and JavaScript-free.

   Narrow-first: the nav is a single stacked column until there is room for a
   grid, and every section body is one column. An admin fixing a server at 1am is
   as likely to be on a phone as at a desk.
   ============================================================================= */

.set-nav {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--mf-space-2);
  margin-bottom: var(--mf-space-5);
}

.set-nav-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--mf-space-3) var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  background: var(--mf-surface);
  color: var(--mf-fg-muted);
  text-decoration: none;
  transition: background var(--mf-dur-fast) var(--mf-ease),
              border-color var(--mf-dur-fast) var(--mf-ease),
              color var(--mf-dur-fast) var(--mf-ease);
}

.set-nav-item:hover {
  background: var(--mf-surface-hover);
  border-color: var(--mf-border-strong);
  color: var(--mf-fg);
}

.set-nav-item:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

/* The active section is marked by the accent border and the brighter label, not by
   a filled block: a solid accent panel next to four quiet ones reads as a button
   somebody forgot to un-press. */
.set-nav-item.is-active {
  border-color: var(--mf-border-accent);
  background: var(--mf-accent-wash);
  color: var(--mf-fg);
}

.set-nav-label {
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  color: inherit;
}

.set-nav-hint {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
}

.set-section {
  display: block;
}

.set-title {
  font-size: var(--mf-text-xl);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-tight);
  margin: 0 0 var(--mf-space-2);
}

.set-lede {
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-md);
  line-height: var(--mf-leading-normal);
  margin: 0 0 var(--mf-space-4);
  max-width: var(--mf-layout-max-prose);
}

.set-subhead {
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  margin: var(--mf-space-6) 0 var(--mf-space-3);
}

/* Every card in a section keeps the same rhythm, so the eye can tell a new block
   from a continuation without reading it. */
.set-card {
  margin-bottom: var(--mf-space-4);
}

.set-alert {
  margin: var(--mf-space-4) 0;
}

.set-note {
  margin: var(--mf-space-3) 0 0;
}

.set-block {
  display: block;
}

.set-figure {
  font-variant-numeric: tabular-nums;
}

/* Right-aligned action cell in the role and token tables. */
.set-cell-end {
  text-align: right;
}

/* Timestamps and counters in a table change between renders; tabular figures stop
   the column jittering as digit widths change. */
.set-num {
  font-variant-numeric: tabular-nums;
}

.set-inline-form {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  align-items: center;
}

.set-inline-form input {
  flex: 1 1 min(240px, 100%);
  min-width: 0;
}

.set-legend,
.set-fieldset legend {
  display: block;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-muted);
  margin-bottom: var(--mf-space-2);
  padding: 0;
}

.set-fieldset {
  border: none;
  margin: 0;
  padding: 0;
  min-width: 0;   /* a fieldset has an intrinsic min-width that ignores flex/grid */
}

.set-checks {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-3) var(--mf-space-5);
  margin-bottom: var(--mf-space-2);
}

.set-check,
.set-scope {
  display: flex;
  align-items: flex-start;
  gap: var(--mf-space-2);
  font-weight: var(--mf-weight-normal);
  cursor: pointer;
}

.set-scope {
  margin-top: var(--mf-space-2);
}

.set-scope input {
  margin-top: 3px;
}

/* The store's own signing key is the one thing on the keys section that is not a
   list item, so it gets the accent edge. */
.set-key-self {
  border-left: 3px solid var(--mf-ok-fg);
}

.set-key-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-3);
  flex-wrap: wrap;
  font-size: var(--mf-text-lg);
  margin: 0 0 var(--mf-space-2);
}

.set-key-row {
  border-left: 3px solid var(--mf-accent);
}

.set-mono,
.set-mono-faint {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-xs);
  overflow-wrap: anywhere;
}

.set-mono-faint {
  color: var(--mf-fg-subtle);
}

.set-fold {
  border-top: 1px solid var(--mf-border);
  padding: var(--mf-space-3) 0 0;
  margin-top: var(--mf-space-3);
}

.set-fold > summary {
  cursor: pointer;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-muted);
}

.set-fold > summary:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
  border-radius: var(--mf-radius-xs);
}

.set-pre {
  overflow-x: auto;
  padding: var(--mf-space-4);
  font-size: var(--mf-text-xs);
  margin: var(--mf-space-3) 0;
}

/* The one-time token. Allowed to break anywhere: it is 40-odd characters of base64
   and the alternative is a page that scrolls sideways on a phone. */
.set-token {
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-md);
  overflow-wrap: anywhere;
  margin: var(--mf-space-3) 0 0;
  padding: var(--mf-space-3);
  border-radius: var(--mf-radius-sm);
  background: var(--mf-bg-sunken);
}

@media (min-width: 760px) {
  /* Five sections across, once there is room for the hint line to stay on one row.
     min(...) floors so the tracks collapse instead of pushing the page wide. */
  .set-nav {
    grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
  }
}

/* =============================================================================
   AUDIT LOG (dashboard/audit.html)
   =============================================================================
   An investigative surface, so the layout is built around scanning a column of
   timestamps and jumping out of it: the time is its own fixed-width gutter, the
   actor and the action are links that narrow the view to themselves, and the
   target is a link to the object the event is about.

   Privileged events carry an accent bar and a tag. That is the whole visual
   separation and it is deliberately not a second list — a log split into two
   chronologies is a log you have to read twice to reconstruct one evening.
   ============================================================================= */

.al-filters {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
  padding: var(--mf-space-4);
  margin-bottom: var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  background: var(--mf-surface);
}

.al-filter-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: var(--mf-space-3);
}

.al-field {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-1);
  min-width: 0;
}

.al-field > span {
  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);
}

.al-field select,
.al-field input {
  width: 100%;
  min-width: 0;
  height: var(--mf-control-height);
  padding: 0 var(--mf-space-3);
  border: 1px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-sm);
  background: var(--mf-bg-sunken);
  color: var(--mf-fg);
  font-family: inherit;
  font-size: var(--mf-text-sm);
}

.al-field select:focus-visible,
.al-field input:focus-visible {
  outline: none;
  border-color: var(--mf-accent);
  box-shadow: var(--mf-focus-ring);
}

.al-field-wide {
  grid-column: 1 / -1;
}

.al-filter-actions {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.al-count {
  margin-left: auto;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-muted);
  /* The figures move on every filter change; tabular digits stop the line shifting. */
  font-variant-numeric: tabular-nums;
}

.al-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--mf-space-2);
  margin-bottom: var(--mf-space-4);
}

.al-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-2);
  padding: var(--mf-space-1) var(--mf-space-3);
  border-radius: var(--mf-radius-full);
  border: 1px solid var(--mf-border-accent);
  background: var(--mf-accent-wash);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xs);
  text-decoration: none;
}

.al-chip b {
  color: var(--mf-fg);
  overflow-wrap: anywhere;
}

.al-chip:hover {
  background: var(--mf-accent-wash-strong);
}

.al-chip:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

.al-lede {
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  margin: 0 0 var(--mf-space-3);
}

.al-figure {
  font-variant-numeric: tabular-nums;
  color: var(--mf-fg);
}

.al-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-lg);
  background: var(--mf-surface);
  overflow: hidden;
}

.al-ev {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--mf-space-1);
  padding: var(--mf-space-3) var(--mf-space-4);
  border-bottom: 1px solid var(--mf-border);
  border-left: 3px solid transparent;
}

.al-ev:last-child {
  border-bottom: none;
}

/* The two states worth a colour. Privileged means "this changed what the store
   vouches for, or what somebody can do"; system means "no human did this", which is
   the row you skip past when you are looking for a person. */
.al-ev.is-privileged {
  border-left-color: var(--mf-accent);
  background: var(--mf-accent-wash);
}

.al-ev.is-system {
  border-left-color: var(--mf-border-strong);
}

.al-when {
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  /* Timestamps are the column the eye runs down; proportional digits make that
     column wobble. */
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.al-clock {
  margin-left: var(--mf-space-2);
  color: var(--mf-fg-muted);
}

.al-body {
  min-width: 0;
}

.al-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin: 0 0 var(--mf-space-1);
}

.al-actor {
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.al-actor:hover {
  color: var(--mf-accent-text);
}

.al-actor:focus-visible,
.al-action:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
  border-radius: var(--mf-radius-full);
}

.al-action {
  text-decoration: none;
}

.al-tag {
  font-size: var(--mf-text-2xs);
  text-transform: uppercase;
  letter-spacing: var(--mf-tracking-wide);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-accent-text);
}

.al-target,
.al-detail,
.al-reason,
.al-ip {
  margin: 0;
  font-size: var(--mf-text-xs);
  /* Module ids, hashes and hostnames are the classic source of a page that scrolls
     sideways on a phone. */
  overflow-wrap: anywhere;
}

.al-target {
  display: flex;
  align-items: baseline;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  font-family: var(--mf-font-mono);
  color: var(--mf-fg);
}

.al-target a {
  color: var(--mf-accent-text);
}

.al-target-filter {
  font-family: var(--mf-font-sans);
  font-size: var(--mf-text-2xs);
  color: var(--mf-fg-subtle);
  text-decoration: none;
}

.al-target-filter:hover {
  color: var(--mf-fg-muted);
  text-decoration: underline;
}

.al-detail {
  color: var(--mf-fg-muted);
  margin-top: 2px;
}

/* The reason is what a human typed, and it is what somebody will be reading in a
   year trying to understand a decision. It is set apart from the detail for that
   reason and not for emphasis. */
.al-reason {
  margin-top: var(--mf-space-2);
  padding-left: var(--mf-space-3);
  border-left: 2px solid var(--mf-border-strong);
  color: var(--mf-fg);
  font-style: italic;
}

.al-ip {
  margin-top: 2px;
  color: var(--mf-fg-subtle);
  font-family: var(--mf-font-mono);
}

.al-pager {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin-top: var(--mf-space-4);
}

.al-pages {
  display: flex;
  gap: var(--mf-space-1);
  flex-wrap: wrap;
}

.al-page {
  min-width: 34px;
  height: var(--mf-control-height-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--mf-space-2);
  border-radius: var(--mf-radius-sm);
  border: 1px solid var(--mf-border);
  color: var(--mf-fg-muted);
  font-size: var(--mf-text-xs);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.al-page:hover {
  background: var(--mf-surface-hover);
  color: var(--mf-fg);
}

.al-page:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
}

.al-page.is-on {
  border-color: var(--mf-border-accent);
  background: var(--mf-accent-wash);
  color: var(--mf-fg);
}

/* A pager arrow with nowhere to go stays in place rather than disappearing: a
   control that vanishes moves the one next to it under the cursor. */
.al-pager .btn.is-off {
  opacity: 0.4;
  pointer-events: none;
}

.al-note {
  margin-top: var(--mf-space-4);
  max-width: var(--mf-layout-max-prose);
}

@media (min-width: 720px) {
  /* The timestamp becomes a gutter once there is room for one. Below this it sits
     above the event, which is the only honest way to fit both on 360px. */
  .al-ev {
    grid-template-columns: 132px minmax(0, 1fr);
    gap: var(--mf-space-4);
    align-items: baseline;
  }

  .al-when {
    display: flex;
    flex-direction: column;
  }

  .al-clock {
    margin-left: 0;
  }
}

/* =============================================================================
   DEV INFO POSTS (dashboard/devinfos.html, devinfos_form.html)
   =============================================================================
   The feed every MediaForge installation polls, and the editor that writes it.

   The editor is one column on a phone — form first, preview under it, because on a
   narrow screen the preview is something you scroll to after typing, not something
   you watch while typing. Above 1100px they sit side by side and the preview
   sticks, which is where watching it while typing becomes worth the width.
   ============================================================================= */

.di-lede {
  font-size: var(--mf-text-sm);
  color: var(--mf-fg-muted);
  margin: 0 0 var(--mf-space-4);
}

.di-alert {
  margin: var(--mf-space-4) 0;
}

.di-errors {
  margin: var(--mf-space-2) 0 var(--mf-space-2);
  padding-left: var(--mf-space-5);
}

.di-list {
  list-style: none;
  margin: var(--mf-space-4) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
}

.di-row {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
  padding: var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-left: 3px solid var(--mf-border-strong);
  border-radius: var(--mf-radius-md);
  background: var(--mf-surface);
}

/* The three publish states, as a colour on the edge and on the dot. "Published but
   the tag resolves to nothing" is the state the old list could not express: it was
   drawn exactly like a healthy published post. */
.di-row.is-published { border-left-color: var(--mf-ok-fg); }
.di-row.is-draft     { border-left-color: var(--mf-border-strong); }
.di-row.is-dangling  { border-left-color: var(--mf-danger-fg); }

.di-main {
  min-width: 0;
}

.di-title {
  margin: 0 0 var(--mf-space-2);
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
}

.di-title a {
  color: var(--mf-fg);
  text-decoration: none;
}

.di-title a:hover {
  color: var(--mf-accent-text);
}

.di-title a:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
  border-radius: var(--mf-radius-xs);
}

.di-state {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--mf-text-sm);
}

.di-state-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--mf-radius-full);
  background: var(--mf-fg-subtle);
  flex: 0 0 auto;
}

.di-row.is-published .di-state-dot { background: var(--mf-ok-fg); }
.di-row.is-dangling .di-state-dot  { background: var(--mf-danger-fg); }

.di-pos {
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-subtle);
  font-variant-numeric: tabular-nums;
}

.di-state-why {
  margin: var(--mf-space-1) 0 0;
}

.di-meta {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin: var(--mf-space-3) 0 0;
  font-size: var(--mf-text-xs);
}

.di-time {
  font-variant-numeric: tabular-nums;
}

.di-langs {
  display: inline-flex;
  align-items: center;
  gap: var(--mf-space-1);
}

.di-lang {
  padding: 1px var(--mf-space-2);
  border-radius: var(--mf-radius-xs);
  border: 1px solid var(--mf-border-strong);
  font-size: var(--mf-text-2xs);
  font-weight: var(--mf-weight-semibold);
  letter-spacing: var(--mf-tracking-wide);
  color: var(--mf-fg-muted);
}

.di-lang-warn {
  font-size: var(--mf-text-2xs);
  color: var(--mf-warn-fg);
}

.di-actions {
  display: flex;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
}

.di-actions form {
  margin: 0;
}

/* ---- the editor ---------------------------------------------------------- */

.di-editor {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--mf-space-5);
  align-items: start;
}

.di-form textarea {
  min-height: 260px;
  font-family: var(--mf-font-mono);
  font-size: var(--mf-text-sm);
  line-height: var(--mf-leading-normal);
}

.di-publish {
  padding: var(--mf-space-4);
  border-radius: var(--mf-radius-md);
  background: var(--mf-bg-sunken);
}

.di-preview {
  display: flex;
  flex-direction: column;
  gap: var(--mf-space-3);
  min-width: 0;
}

.di-preview-head h2 {
  font-size: var(--mf-text-md);
  font-weight: var(--mf-weight-semibold);
  margin: 0 0 var(--mf-space-1);
}

.di-preview-head p {
  margin: 0;
}

.di-langline {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--mf-text-xs);
  color: var(--mf-fg-muted);
}

/* A mock-up of MediaForge's own Dev Infos card, not of this dashboard. The sunken
   background is what says "this is somebody else's screen". */
.di-card {
  padding: var(--mf-space-4);
  border: 1px solid var(--mf-border);
  border-radius: var(--mf-radius-md);
  background: var(--mf-bg-sunken);
  min-width: 0;
}

.di-card-chrome {
  margin-bottom: var(--mf-space-3);
  padding-bottom: var(--mf-space-2);
  border-bottom: 1px dashed var(--mf-border-strong);
}

.di-card-locale {
  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);
}

.di-card-head {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--mf-space-2);
}

.di-card-title {
  margin: 0 0 var(--mf-space-3);
  font-size: var(--mf-text-lg);
  font-weight: var(--mf-weight-semibold);
  line-height: var(--mf-leading-snug);
  overflow-wrap: anywhere;
}

.di-card-release {
  display: flex;
  align-items: center;
  gap: var(--mf-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--mf-space-3);
  padding: var(--mf-space-2) var(--mf-space-3);
  border-radius: var(--mf-radius-sm);
  background: var(--mf-surface);
}

.di-card-changelog {
  margin-top: var(--mf-space-3);
  padding-top: var(--mf-space-3);
  border-top: 1px solid var(--mf-border);
}

.di-card-changelog > summary {
  cursor: pointer;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-muted);
}

.di-card-changelog > summary:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
  border-radius: var(--mf-radius-xs);
}

.di-payload > summary {
  cursor: pointer;
  font-size: var(--mf-text-sm);
  font-weight: var(--mf-weight-semibold);
  color: var(--mf-fg-muted);
}

.di-payload > summary:focus-visible {
  outline: none;
  box-shadow: var(--mf-focus-ring);
  border-radius: var(--mf-radius-xs);
}

.di-pre {
  overflow-x: auto;
  padding: var(--mf-space-3);
  margin: var(--mf-space-3) 0;
  font-size: var(--mf-text-xs);
}

@media (min-width: 900px) {
  /* The list row puts its actions on the right once there is room for them not to
     crowd the metadata. */
  .di-row {
    flex-direction: row;
    align-items: flex-start;
    justify-content: space-between;
  }

  .di-actions {
    flex: 0 0 auto;
  }
}

@media (min-width: 1100px) {
  .di-editor {
    grid-template-columns: minmax(min(420px, 100%), 1fr) minmax(min(380px, 100%), 1fr);
  }

  /* Sticky only where the viewport is tall enough for it not to become the whole
     screen. Below the topbar, so it does not slide underneath it. */
  .di-preview {
    position: sticky;
    top: calc(var(--mf-layout-topbar) + var(--mf-space-4));
    max-height: calc(100vh - var(--mf-layout-topbar) - var(--mf-space-6));
    overflow-y: auto;
  }
}
